Initial commit
This commit is contained in:
442
.venv/Lib/site-packages/django/core/management/__init__.py
Normal file
442
.venv/Lib/site-packages/django/core/management/__init__.py
Normal file
@@ -0,0 +1,442 @@
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
from argparse import (
|
||||
_AppendConstAction,
|
||||
_CountAction,
|
||||
_StoreConstAction,
|
||||
_SubParsersAction,
|
||||
)
|
||||
from collections import defaultdict
|
||||
from difflib import get_close_matches
|
||||
from importlib import import_module
|
||||
|
||||
import django
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management.base import (
|
||||
BaseCommand,
|
||||
CommandError,
|
||||
CommandParser,
|
||||
handle_default_options,
|
||||
)
|
||||
from django.core.management.color import color_style
|
||||
from django.utils import autoreload
|
||||
|
||||
|
||||
def find_commands(management_dir):
|
||||
"""
|
||||
Given a path to a management directory, return a list of all the command
|
||||
names that are available.
|
||||
"""
|
||||
command_dir = os.path.join(management_dir, "commands")
|
||||
return [
|
||||
name
|
||||
for _, name, is_pkg in pkgutil.iter_modules([command_dir])
|
||||
if not is_pkg and not name.startswith("_")
|
||||
]
|
||||
|
||||
|
||||
def load_command_class(app_name, name):
|
||||
"""
|
||||
Given a command name and an application name, return the Command
|
||||
class instance. Allow all errors raised by the import process
|
||||
(ImportError, AttributeError) to propagate.
|
||||
"""
|
||||
module = import_module("%s.management.commands.%s" % (app_name, name))
|
||||
return module.Command()
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_commands():
|
||||
"""
|
||||
Return a dictionary mapping command names to their callback applications.
|
||||
|
||||
Look for a management.commands package in django.core, and in each
|
||||
installed application -- if a commands package exists, register all
|
||||
commands in that package.
|
||||
|
||||
Core commands are always included. If a settings module has been
|
||||
specified, also include user-defined commands.
|
||||
|
||||
The dictionary is in the format {command_name: app_name}. Key-value
|
||||
pairs from this dictionary can then be used in calls to
|
||||
load_command_class(app_name, command_name)
|
||||
|
||||
The dictionary is cached on the first call and reused on subsequent
|
||||
calls.
|
||||
"""
|
||||
commands = {name: "django.core" for name in find_commands(__path__[0])}
|
||||
|
||||
if not settings.configured:
|
||||
return commands
|
||||
|
||||
for app_config in reversed(apps.get_app_configs()):
|
||||
path = os.path.join(app_config.path, "management")
|
||||
commands.update({name: app_config.name for name in find_commands(path)})
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def call_command(command_name, *args, **options):
|
||||
"""
|
||||
Call the given command, with the given options and args/kwargs.
|
||||
|
||||
This is the primary API you should use for calling specific commands.
|
||||
|
||||
`command_name` may be a string or a command object. Using a string is
|
||||
preferred unless the command object is required for further processing or
|
||||
testing.
|
||||
|
||||
Some examples:
|
||||
call_command('migrate')
|
||||
call_command('shell', plain=True)
|
||||
call_command('sqlmigrate', 'myapp')
|
||||
|
||||
from django.core.management.commands import flush
|
||||
cmd = flush.Command()
|
||||
call_command(cmd, verbosity=0, interactive=False)
|
||||
# Do something with cmd ...
|
||||
"""
|
||||
if isinstance(command_name, BaseCommand):
|
||||
# Command object passed in.
|
||||
command = command_name
|
||||
command_name = command.__class__.__module__.split(".")[-1]
|
||||
else:
|
||||
# Load the command object by name.
|
||||
try:
|
||||
app_name = get_commands()[command_name]
|
||||
except KeyError:
|
||||
raise CommandError("Unknown command: %r" % command_name)
|
||||
|
||||
if isinstance(app_name, BaseCommand):
|
||||
# If the command is already loaded, use it directly.
|
||||
command = app_name
|
||||
else:
|
||||
command = load_command_class(app_name, command_name)
|
||||
|
||||
# Simulate argument parsing to get the option defaults (see #10080 for details).
|
||||
parser = command.create_parser("", command_name)
|
||||
# Use the `dest` option name from the parser option
|
||||
opt_mapping = {
|
||||
min(s_opt.option_strings).lstrip("-").replace("-", "_"): s_opt.dest
|
||||
for s_opt in parser._actions
|
||||
if s_opt.option_strings
|
||||
}
|
||||
arg_options = {opt_mapping.get(key, key): value for key, value in options.items()}
|
||||
parse_args = []
|
||||
for arg in args:
|
||||
if isinstance(arg, (list, tuple)):
|
||||
parse_args += map(str, arg)
|
||||
else:
|
||||
parse_args.append(str(arg))
|
||||
|
||||
def get_actions(parser):
|
||||
# Parser actions and actions from sub-parser choices.
|
||||
for opt in parser._actions:
|
||||
if isinstance(opt, _SubParsersAction):
|
||||
for sub_opt in opt.choices.values():
|
||||
yield from get_actions(sub_opt)
|
||||
else:
|
||||
yield opt
|
||||
|
||||
parser_actions = list(get_actions(parser))
|
||||
mutually_exclusive_required_options = {
|
||||
opt
|
||||
for group in parser._mutually_exclusive_groups
|
||||
for opt in group._group_actions
|
||||
if group.required
|
||||
}
|
||||
# Any required arguments which are passed in via **options must be passed
|
||||
# to parse_args().
|
||||
for opt in parser_actions:
|
||||
if opt.dest in options and (
|
||||
opt.required or opt in mutually_exclusive_required_options
|
||||
):
|
||||
opt_dest_count = sum(v == opt.dest for v in opt_mapping.values())
|
||||
if opt_dest_count > 1:
|
||||
raise TypeError(
|
||||
f"Cannot pass the dest {opt.dest!r} that matches multiple "
|
||||
f"arguments via **options."
|
||||
)
|
||||
parse_args.append(min(opt.option_strings))
|
||||
if isinstance(opt, (_AppendConstAction, _CountAction, _StoreConstAction)):
|
||||
continue
|
||||
value = arg_options[opt.dest]
|
||||
if isinstance(value, (list, tuple)):
|
||||
parse_args += map(str, value)
|
||||
else:
|
||||
parse_args.append(str(value))
|
||||
defaults = parser.parse_args(args=parse_args)
|
||||
defaults = dict(defaults._get_kwargs(), **arg_options)
|
||||
# Raise an error if any unknown options were passed.
|
||||
stealth_options = set(command.base_stealth_options + command.stealth_options)
|
||||
dest_parameters = {action.dest for action in parser_actions}
|
||||
valid_options = (dest_parameters | stealth_options).union(opt_mapping)
|
||||
unknown_options = set(options) - valid_options
|
||||
if unknown_options:
|
||||
raise TypeError(
|
||||
"Unknown option(s) for %s command: %s. "
|
||||
"Valid options are: %s."
|
||||
% (
|
||||
command_name,
|
||||
", ".join(sorted(unknown_options)),
|
||||
", ".join(sorted(valid_options)),
|
||||
)
|
||||
)
|
||||
# Move positional args out of options to mimic legacy optparse
|
||||
args = defaults.pop("args", ())
|
||||
if "skip_checks" not in options:
|
||||
defaults["skip_checks"] = True
|
||||
|
||||
return command.execute(*args, **defaults)
|
||||
|
||||
|
||||
class ManagementUtility:
|
||||
"""
|
||||
Encapsulate the logic of the django-admin and manage.py utilities.
|
||||
"""
|
||||
|
||||
def __init__(self, argv=None):
|
||||
self.argv = argv or sys.argv[:]
|
||||
self.prog_name = os.path.basename(self.argv[0])
|
||||
if self.prog_name == "__main__.py":
|
||||
self.prog_name = "python -m django"
|
||||
self.settings_exception = None
|
||||
|
||||
def main_help_text(self, commands_only=False):
|
||||
"""Return the script's main help text, as a string."""
|
||||
if commands_only:
|
||||
usage = sorted(get_commands())
|
||||
else:
|
||||
usage = [
|
||||
"",
|
||||
"Type '%s help <subcommand>' for help on a specific subcommand."
|
||||
% self.prog_name,
|
||||
"",
|
||||
"Available subcommands:",
|
||||
]
|
||||
commands_dict = defaultdict(lambda: [])
|
||||
for name, app in get_commands().items():
|
||||
if app == "django.core":
|
||||
app = "django"
|
||||
else:
|
||||
app = app.rpartition(".")[-1]
|
||||
commands_dict[app].append(name)
|
||||
style = color_style()
|
||||
for app in sorted(commands_dict):
|
||||
usage.append("")
|
||||
usage.append(style.NOTICE("[%s]" % app))
|
||||
for name in sorted(commands_dict[app]):
|
||||
usage.append(" %s" % name)
|
||||
# Output an extra note if settings are not properly configured
|
||||
if self.settings_exception is not None:
|
||||
usage.append(
|
||||
style.NOTICE(
|
||||
"Note that only Django core commands are listed "
|
||||
"as settings are not properly configured (error: %s)."
|
||||
% self.settings_exception
|
||||
)
|
||||
)
|
||||
|
||||
return "\n".join(usage)
|
||||
|
||||
def fetch_command(self, subcommand):
|
||||
"""
|
||||
Try to fetch the given subcommand, printing a message with the
|
||||
appropriate command called from the command line (usually
|
||||
"django-admin" or "manage.py") if it can't be found.
|
||||
"""
|
||||
# Get commands outside of try block to prevent swallowing exceptions
|
||||
commands = get_commands()
|
||||
try:
|
||||
app_name = commands[subcommand]
|
||||
except KeyError:
|
||||
if os.environ.get("DJANGO_SETTINGS_MODULE"):
|
||||
# If `subcommand` is missing due to misconfigured settings, the
|
||||
# following line will retrigger an ImproperlyConfigured exception
|
||||
# (get_commands() swallows the original one) so the user is
|
||||
# informed about it.
|
||||
settings.INSTALLED_APPS
|
||||
elif not settings.configured:
|
||||
sys.stderr.write("No Django settings specified.\n")
|
||||
possible_matches = get_close_matches(subcommand, commands)
|
||||
sys.stderr.write("Unknown command: %r" % subcommand)
|
||||
if possible_matches:
|
||||
sys.stderr.write(". Did you mean %s?" % possible_matches[0])
|
||||
sys.stderr.write("\nType '%s help' for usage.\n" % self.prog_name)
|
||||
sys.exit(1)
|
||||
if isinstance(app_name, BaseCommand):
|
||||
# If the command is already loaded, use it directly.
|
||||
klass = app_name
|
||||
else:
|
||||
klass = load_command_class(app_name, subcommand)
|
||||
return klass
|
||||
|
||||
def autocomplete(self):
|
||||
"""
|
||||
Output completion suggestions for BASH.
|
||||
|
||||
The output of this function is passed to BASH's `COMPREPLY` variable
|
||||
and treated as completion suggestions. `COMPREPLY` expects a space
|
||||
separated string as the result.
|
||||
|
||||
The `COMP_WORDS` and `COMP_CWORD` BASH environment variables are used
|
||||
to get information about the cli input. Please refer to the BASH
|
||||
man-page for more information about this variables.
|
||||
|
||||
Subcommand options are saved as pairs. A pair consists of
|
||||
the long option string (e.g. '--exclude') and a boolean
|
||||
value indicating if the option requires arguments. When printing to
|
||||
stdout, an equal sign is appended to options which require arguments.
|
||||
|
||||
Note: If debugging this function, it is recommended to write the debug
|
||||
output in a separate file. Otherwise the debug output will be treated
|
||||
and formatted as potential completion suggestions.
|
||||
"""
|
||||
# Don't complete if user hasn't sourced bash_completion file.
|
||||
if "DJANGO_AUTO_COMPLETE" not in os.environ:
|
||||
return
|
||||
|
||||
cwords = os.environ["COMP_WORDS"].split()[1:]
|
||||
cword = int(os.environ["COMP_CWORD"])
|
||||
|
||||
try:
|
||||
curr = cwords[cword - 1]
|
||||
except IndexError:
|
||||
curr = ""
|
||||
|
||||
subcommands = [*get_commands(), "help"]
|
||||
options = [("--help", False)]
|
||||
|
||||
# subcommand
|
||||
if cword == 1:
|
||||
print(" ".join(sorted(filter(lambda x: x.startswith(curr), subcommands))))
|
||||
# subcommand options
|
||||
# special case: the 'help' subcommand has no options
|
||||
elif cwords[0] in subcommands and cwords[0] != "help":
|
||||
subcommand_cls = self.fetch_command(cwords[0])
|
||||
# special case: add the names of installed apps to options
|
||||
if cwords[0] in ("dumpdata", "sqlmigrate", "sqlsequencereset", "test"):
|
||||
try:
|
||||
app_configs = apps.get_app_configs()
|
||||
# Get the last part of the dotted path as the app name.
|
||||
options.extend((app_config.label, 0) for app_config in app_configs)
|
||||
except ImportError:
|
||||
# Fail silently if DJANGO_SETTINGS_MODULE isn't set. The
|
||||
# user will find out once they execute the command.
|
||||
pass
|
||||
parser = subcommand_cls.create_parser("", cwords[0])
|
||||
options.extend(
|
||||
(min(s_opt.option_strings), s_opt.nargs != 0)
|
||||
for s_opt in parser._actions
|
||||
if s_opt.option_strings
|
||||
)
|
||||
# filter out previously specified options from available options
|
||||
prev_opts = {x.split("=")[0] for x in cwords[1 : cword - 1]}
|
||||
options = (opt for opt in options if opt[0] not in prev_opts)
|
||||
|
||||
# filter options by current input
|
||||
options = sorted((k, v) for k, v in options if k.startswith(curr))
|
||||
for opt_label, require_arg in options:
|
||||
# append '=' to options which require args
|
||||
if require_arg:
|
||||
opt_label += "="
|
||||
print(opt_label)
|
||||
# Exit code of the bash completion function is never passed back to
|
||||
# the user, so it's safe to always exit with 0.
|
||||
# For more details see #25420.
|
||||
sys.exit(0)
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
Given the command-line arguments, figure out which subcommand is being
|
||||
run, create a parser appropriate to that command, and run it.
|
||||
"""
|
||||
try:
|
||||
subcommand = self.argv[1]
|
||||
except IndexError:
|
||||
subcommand = "help" # Display help if no arguments were given.
|
||||
|
||||
# Preprocess options to extract --settings and --pythonpath.
|
||||
# These options could affect the commands that are available, so they
|
||||
# must be processed early.
|
||||
parser = CommandParser(
|
||||
prog=self.prog_name,
|
||||
usage="%(prog)s subcommand [options] [args]",
|
||||
add_help=False,
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("--settings")
|
||||
parser.add_argument("--pythonpath")
|
||||
parser.add_argument("args", nargs="*") # catch-all
|
||||
try:
|
||||
options, args = parser.parse_known_args(self.argv[2:])
|
||||
handle_default_options(options)
|
||||
except CommandError:
|
||||
pass # Ignore any option errors at this point.
|
||||
|
||||
try:
|
||||
settings.INSTALLED_APPS
|
||||
except ImproperlyConfigured as exc:
|
||||
self.settings_exception = exc
|
||||
except ImportError as exc:
|
||||
self.settings_exception = exc
|
||||
|
||||
if settings.configured:
|
||||
# Start the auto-reloading dev server even if the code is broken.
|
||||
# The hardcoded condition is a code smell but we can't rely on a
|
||||
# flag on the command class because we haven't located it yet.
|
||||
if subcommand == "runserver" and "--noreload" not in self.argv:
|
||||
try:
|
||||
autoreload.check_errors(django.setup)()
|
||||
except Exception:
|
||||
# The exception will be raised later in the child process
|
||||
# started by the autoreloader. Pretend it didn't happen by
|
||||
# loading an empty list of applications.
|
||||
apps.all_models = defaultdict(dict)
|
||||
apps.app_configs = {}
|
||||
apps.apps_ready = apps.models_ready = apps.ready = True
|
||||
|
||||
# Remove options not compatible with the built-in runserver
|
||||
# (e.g. options for the contrib.staticfiles' runserver).
|
||||
# Changes here require manually testing as described in
|
||||
# #27522.
|
||||
_parser = self.fetch_command("runserver").create_parser(
|
||||
"django", "runserver"
|
||||
)
|
||||
_options, _args = _parser.parse_known_args(self.argv[2:])
|
||||
for _arg in _args:
|
||||
self.argv.remove(_arg)
|
||||
|
||||
# In all other cases, django.setup() is required to succeed.
|
||||
else:
|
||||
django.setup()
|
||||
|
||||
self.autocomplete()
|
||||
|
||||
if subcommand == "help":
|
||||
if "--commands" in args:
|
||||
sys.stdout.write(self.main_help_text(commands_only=True) + "\n")
|
||||
elif not options.args:
|
||||
sys.stdout.write(self.main_help_text() + "\n")
|
||||
else:
|
||||
self.fetch_command(options.args[0]).print_help(
|
||||
self.prog_name, options.args[0]
|
||||
)
|
||||
# Special-cases: We want 'django-admin --version' and
|
||||
# 'django-admin --help' to work, for backwards compatibility.
|
||||
elif subcommand == "version" or self.argv[1:] == ["--version"]:
|
||||
sys.stdout.write(django.get_version() + "\n")
|
||||
elif self.argv[1:] in (["--help"], ["-h"]):
|
||||
sys.stdout.write(self.main_help_text() + "\n")
|
||||
else:
|
||||
self.fetch_command(subcommand).run_from_argv(self.argv)
|
||||
|
||||
|
||||
def execute_from_command_line(argv=None):
|
||||
"""Run a ManagementUtility."""
|
||||
utility = ManagementUtility(argv)
|
||||
utility.execute()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
707
.venv/Lib/site-packages/django/core/management/base.py
Normal file
707
.venv/Lib/site-packages/django/core/management/base.py
Normal file
@@ -0,0 +1,707 @@
|
||||
"""
|
||||
Base classes for writing management commands (named commands which can
|
||||
be executed through ``django-admin`` or ``manage.py``).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser, HelpFormatter
|
||||
from functools import partial
|
||||
from io import TextIOBase
|
||||
|
||||
import django
|
||||
from django.core import checks
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management.color import color_style, no_style
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
|
||||
ALL_CHECKS = "__all__"
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
"""
|
||||
Exception class indicating a problem while executing a management
|
||||
command.
|
||||
|
||||
If this exception is raised during the execution of a management
|
||||
command, it will be caught and turned into a nicely-printed error
|
||||
message to the appropriate output stream (i.e., stderr); as a
|
||||
result, raising this exception (with a sensible description of the
|
||||
error) is the preferred way to indicate that something has gone
|
||||
wrong in the execution of a command.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, returncode=1, **kwargs):
|
||||
self.returncode = returncode
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class SystemCheckError(CommandError):
|
||||
"""
|
||||
The system check framework detected unrecoverable errors.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CommandParser(ArgumentParser):
|
||||
"""
|
||||
Customized ArgumentParser class to improve some error messages and prevent
|
||||
SystemExit in several occasions, as SystemExit is unacceptable when a
|
||||
command is called programmatically.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *, missing_args_message=None, called_from_command_line=None, **kwargs
|
||||
):
|
||||
self.missing_args_message = missing_args_message
|
||||
self.called_from_command_line = called_from_command_line
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
# Catch missing argument for a better error message
|
||||
if self.missing_args_message and not (
|
||||
args or any(not arg.startswith("-") for arg in args)
|
||||
):
|
||||
self.error(self.missing_args_message)
|
||||
return super().parse_args(args, namespace)
|
||||
|
||||
def error(self, message):
|
||||
if self.called_from_command_line:
|
||||
super().error(message)
|
||||
else:
|
||||
raise CommandError("Error: %s" % message)
|
||||
|
||||
def add_subparsers(self, **kwargs):
|
||||
parser_class = kwargs.get("parser_class", type(self))
|
||||
if issubclass(parser_class, CommandParser):
|
||||
kwargs["parser_class"] = partial(
|
||||
parser_class,
|
||||
called_from_command_line=self.called_from_command_line,
|
||||
)
|
||||
return super().add_subparsers(**kwargs)
|
||||
|
||||
|
||||
def handle_default_options(options):
|
||||
"""
|
||||
Include any default options that all commands should accept here
|
||||
so that ManagementUtility can handle them before searching for
|
||||
user commands.
|
||||
"""
|
||||
if options.settings:
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = options.settings
|
||||
if options.pythonpath:
|
||||
sys.path.insert(0, options.pythonpath)
|
||||
|
||||
|
||||
def no_translations(handle_func):
|
||||
"""Decorator that forces a command to run with translations deactivated."""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
from django.utils import translation
|
||||
|
||||
saved_locale = translation.get_language()
|
||||
translation.deactivate_all()
|
||||
try:
|
||||
res = handle_func(*args, **kwargs)
|
||||
finally:
|
||||
if saved_locale is not None:
|
||||
translation.activate(saved_locale)
|
||||
return res
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class DjangoHelpFormatter(HelpFormatter):
|
||||
"""
|
||||
Customized formatter so that command-specific arguments appear in the
|
||||
--help output before arguments common to all commands.
|
||||
"""
|
||||
|
||||
show_last = {
|
||||
"--version",
|
||||
"--verbosity",
|
||||
"--traceback",
|
||||
"--settings",
|
||||
"--pythonpath",
|
||||
"--no-color",
|
||||
"--force-color",
|
||||
"--skip-checks",
|
||||
}
|
||||
|
||||
def _reordered_actions(self, actions):
|
||||
return sorted(
|
||||
actions, key=lambda a: set(a.option_strings) & self.show_last != set()
|
||||
)
|
||||
|
||||
def add_usage(self, usage, actions, *args, **kwargs):
|
||||
super().add_usage(usage, self._reordered_actions(actions), *args, **kwargs)
|
||||
|
||||
def add_arguments(self, actions):
|
||||
super().add_arguments(self._reordered_actions(actions))
|
||||
|
||||
|
||||
class OutputWrapper:
|
||||
"""
|
||||
Wrapper around stdout/stderr
|
||||
"""
|
||||
|
||||
@property
|
||||
def style_func(self):
|
||||
return self._style_func
|
||||
|
||||
@style_func.setter
|
||||
def style_func(self, style_func):
|
||||
if style_func and self.isatty():
|
||||
self._style_func = style_func
|
||||
else:
|
||||
self._style_func = lambda x: x
|
||||
|
||||
def __init__(self, out, ending="\n"):
|
||||
self._out = out
|
||||
self.style_func = None
|
||||
self.ending = ending
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._out, name)
|
||||
|
||||
def flush(self):
|
||||
if hasattr(self._out, "flush"):
|
||||
self._out.flush()
|
||||
|
||||
def isatty(self):
|
||||
return hasattr(self._out, "isatty") and self._out.isatty()
|
||||
|
||||
def write(self, msg="", style_func=None, ending=None):
|
||||
ending = self.ending if ending is None else ending
|
||||
if ending and not msg.endswith(ending):
|
||||
msg += ending
|
||||
style_func = style_func or self.style_func
|
||||
self._out.write(style_func(msg))
|
||||
|
||||
|
||||
TextIOBase.register(OutputWrapper)
|
||||
|
||||
|
||||
class BaseCommand:
|
||||
"""
|
||||
The base class from which all management commands ultimately
|
||||
derive.
|
||||
|
||||
Use this class if you want access to all of the mechanisms which
|
||||
parse the command-line arguments and work out what code to call in
|
||||
response; if you don't need to change any of that behavior,
|
||||
consider using one of the subclasses defined in this file.
|
||||
|
||||
If you are interested in overriding/customizing various aspects of
|
||||
the command-parsing and -execution behavior, the normal flow works
|
||||
as follows:
|
||||
|
||||
1. ``django-admin`` or ``manage.py`` loads the command class
|
||||
and calls its ``run_from_argv()`` method.
|
||||
|
||||
2. The ``run_from_argv()`` method calls ``create_parser()`` to get
|
||||
an ``ArgumentParser`` for the arguments, parses them, performs
|
||||
any environment changes requested by options like
|
||||
``pythonpath``, and then calls the ``execute()`` method,
|
||||
passing the parsed arguments.
|
||||
|
||||
3. The ``execute()`` method attempts to carry out the command by
|
||||
calling the ``handle()`` method with the parsed arguments; any
|
||||
output produced by ``handle()`` will be printed to standard
|
||||
output and, if the command is intended to produce a block of
|
||||
SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``.
|
||||
|
||||
4. If ``handle()`` or ``execute()`` raised any exception (e.g.
|
||||
``CommandError``), ``run_from_argv()`` will instead print an error
|
||||
message to ``stderr``.
|
||||
|
||||
Thus, the ``handle()`` method is typically the starting point for
|
||||
subclasses; many built-in commands and command types either place
|
||||
all of their logic in ``handle()``, or perform some additional
|
||||
parsing work in ``handle()`` and then delegate from it to more
|
||||
specialized methods as needed.
|
||||
|
||||
Several attributes affect behavior at various steps along the way:
|
||||
|
||||
``help``
|
||||
A short description of the command, which will be printed in
|
||||
help messages.
|
||||
|
||||
``output_transaction``
|
||||
A boolean indicating whether the command outputs SQL
|
||||
statements; if ``True``, the output will automatically be
|
||||
wrapped with ``BEGIN;`` and ``COMMIT;``. Default value is
|
||||
``False``.
|
||||
|
||||
``requires_migrations_checks``
|
||||
A boolean; if ``True``, the command prints a warning if the set of
|
||||
migrations on disk don't match the migrations in the database.
|
||||
|
||||
``requires_system_checks``
|
||||
A list or tuple of tags, e.g. [Tags.staticfiles, Tags.models]. System
|
||||
checks registered in the chosen tags will be checked for errors prior
|
||||
to executing the command. The value '__all__' can be used to specify
|
||||
that all system checks should be performed. Default value is '__all__'.
|
||||
|
||||
To validate an individual application's models
|
||||
rather than all applications' models, call
|
||||
``self.check(app_configs)`` from ``handle()``, where ``app_configs``
|
||||
is the list of application's configuration provided by the
|
||||
app registry.
|
||||
|
||||
``stealth_options``
|
||||
A tuple of any options the command uses which aren't defined by the
|
||||
argument parser.
|
||||
"""
|
||||
|
||||
# Metadata about this command.
|
||||
help = ""
|
||||
|
||||
# Configuration shortcuts that alter various logic.
|
||||
_called_from_command_line = False
|
||||
output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
|
||||
requires_migrations_checks = False
|
||||
requires_system_checks = "__all__"
|
||||
# Arguments, common to all commands, which aren't defined by the argument
|
||||
# parser.
|
||||
base_stealth_options = ("stderr", "stdout")
|
||||
# Command-specific options not defined by the argument parser.
|
||||
stealth_options = ()
|
||||
suppressed_base_arguments = set()
|
||||
|
||||
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
|
||||
self.stdout = OutputWrapper(stdout or sys.stdout)
|
||||
self.stderr = OutputWrapper(stderr or sys.stderr)
|
||||
if no_color and force_color:
|
||||
raise CommandError("'no_color' and 'force_color' can't be used together.")
|
||||
if no_color:
|
||||
self.style = no_style()
|
||||
else:
|
||||
self.style = color_style(force_color)
|
||||
self.stderr.style_func = self.style.ERROR
|
||||
if (
|
||||
not isinstance(self.requires_system_checks, (list, tuple))
|
||||
and self.requires_system_checks != ALL_CHECKS
|
||||
):
|
||||
raise TypeError("requires_system_checks must be a list or tuple.")
|
||||
|
||||
def get_version(self):
|
||||
"""
|
||||
Return the Django version, which should be correct for all built-in
|
||||
Django commands. User-supplied commands can override this method to
|
||||
return their own version.
|
||||
"""
|
||||
return django.get_version()
|
||||
|
||||
def create_parser(self, prog_name, subcommand, **kwargs):
|
||||
"""
|
||||
Create and return the ``ArgumentParser`` which will be used to
|
||||
parse the arguments to this command.
|
||||
"""
|
||||
kwargs.setdefault("formatter_class", DjangoHelpFormatter)
|
||||
parser = CommandParser(
|
||||
prog="%s %s" % (os.path.basename(prog_name), subcommand),
|
||||
description=self.help or None,
|
||||
missing_args_message=getattr(self, "missing_args_message", None),
|
||||
called_from_command_line=getattr(self, "_called_from_command_line", None),
|
||||
**kwargs,
|
||||
)
|
||||
self.add_base_argument(
|
||||
parser,
|
||||
"--version",
|
||||
action="version",
|
||||
version=self.get_version(),
|
||||
help="Show program's version number and exit.",
|
||||
)
|
||||
self.add_base_argument(
|
||||
parser,
|
||||
"-v",
|
||||
"--verbosity",
|
||||
default=1,
|
||||
type=int,
|
||||
choices=[0, 1, 2, 3],
|
||||
help=(
|
||||
"Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, "
|
||||
"3=very verbose output"
|
||||
),
|
||||
)
|
||||
self.add_base_argument(
|
||||
parser,
|
||||
"--settings",
|
||||
help=(
|
||||
"The Python path to a settings module, e.g. "
|
||||
'"myproject.settings.main". If this isn\'t provided, the '
|
||||
"DJANGO_SETTINGS_MODULE environment variable will be used."
|
||||
),
|
||||
)
|
||||
self.add_base_argument(
|
||||
parser,
|
||||
"--pythonpath",
|
||||
help=(
|
||||
"A directory to add to the Python path, e.g. "
|
||||
'"/home/djangoprojects/myproject".'
|
||||
),
|
||||
)
|
||||
self.add_base_argument(
|
||||
parser,
|
||||
"--traceback",
|
||||
action="store_true",
|
||||
help="Display a full stack trace on CommandError exceptions.",
|
||||
)
|
||||
self.add_base_argument(
|
||||
parser,
|
||||
"--no-color",
|
||||
action="store_true",
|
||||
help="Don't colorize the command output.",
|
||||
)
|
||||
self.add_base_argument(
|
||||
parser,
|
||||
"--force-color",
|
||||
action="store_true",
|
||||
help="Force colorization of the command output.",
|
||||
)
|
||||
if self.requires_system_checks:
|
||||
parser.add_argument(
|
||||
"--skip-checks",
|
||||
action="store_true",
|
||||
help="Skip system checks.",
|
||||
)
|
||||
self.add_arguments(parser)
|
||||
return parser
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""
|
||||
Entry point for subclassed commands to add custom arguments.
|
||||
"""
|
||||
pass
|
||||
|
||||
def add_base_argument(self, parser, *args, **kwargs):
|
||||
"""
|
||||
Call the parser's add_argument() method, suppressing the help text
|
||||
according to BaseCommand.suppressed_base_arguments.
|
||||
"""
|
||||
for arg in args:
|
||||
if arg in self.suppressed_base_arguments:
|
||||
kwargs["help"] = argparse.SUPPRESS
|
||||
break
|
||||
parser.add_argument(*args, **kwargs)
|
||||
|
||||
def print_help(self, prog_name, subcommand):
|
||||
"""
|
||||
Print the help message for this command, derived from
|
||||
``self.usage()``.
|
||||
"""
|
||||
parser = self.create_parser(prog_name, subcommand)
|
||||
parser.print_help()
|
||||
|
||||
def run_from_argv(self, argv):
|
||||
"""
|
||||
Set up any environment changes requested (e.g., Python path
|
||||
and Django settings), then run this command. If the
|
||||
command raises a ``CommandError``, intercept it and print it sensibly
|
||||
to stderr. If the ``--traceback`` option is present or the raised
|
||||
``Exception`` is not ``CommandError``, raise it.
|
||||
"""
|
||||
self._called_from_command_line = True
|
||||
parser = self.create_parser(argv[0], argv[1])
|
||||
|
||||
options = parser.parse_args(argv[2:])
|
||||
cmd_options = vars(options)
|
||||
# Move positional args out of options to mimic legacy optparse
|
||||
args = cmd_options.pop("args", ())
|
||||
handle_default_options(options)
|
||||
try:
|
||||
self.execute(*args, **cmd_options)
|
||||
except CommandError as e:
|
||||
if options.traceback:
|
||||
raise
|
||||
|
||||
# SystemCheckError takes care of its own formatting.
|
||||
if isinstance(e, SystemCheckError):
|
||||
self.stderr.write(str(e), lambda x: x)
|
||||
else:
|
||||
self.stderr.write("%s: %s" % (e.__class__.__name__, e))
|
||||
sys.exit(e.returncode)
|
||||
finally:
|
||||
try:
|
||||
connections.close_all()
|
||||
except ImproperlyConfigured:
|
||||
# Ignore if connections aren't setup at this point (e.g. no
|
||||
# configured settings).
|
||||
pass
|
||||
|
||||
def execute(self, *args, **options):
|
||||
"""
|
||||
Try to execute this command, performing system checks if needed (as
|
||||
controlled by the ``requires_system_checks`` attribute, except if
|
||||
force-skipped).
|
||||
"""
|
||||
if options["force_color"] and options["no_color"]:
|
||||
raise CommandError(
|
||||
"The --no-color and --force-color options can't be used together."
|
||||
)
|
||||
if options["force_color"]:
|
||||
self.style = color_style(force_color=True)
|
||||
elif options["no_color"]:
|
||||
self.style = no_style()
|
||||
self.stderr.style_func = None
|
||||
if options.get("stdout"):
|
||||
self.stdout = OutputWrapper(options["stdout"])
|
||||
if options.get("stderr"):
|
||||
self.stderr = OutputWrapper(options["stderr"])
|
||||
|
||||
if self.requires_system_checks and not options["skip_checks"]:
|
||||
check_kwargs = self.get_check_kwargs(options)
|
||||
self.check(**check_kwargs)
|
||||
if self.requires_migrations_checks:
|
||||
self.check_migrations()
|
||||
output = self.handle(*args, **options)
|
||||
if output:
|
||||
if self.output_transaction:
|
||||
connection = connections[options.get("database", DEFAULT_DB_ALIAS)]
|
||||
output = "%s\n%s\n%s" % (
|
||||
self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
|
||||
output,
|
||||
self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
|
||||
)
|
||||
self.stdout.write(output)
|
||||
return output
|
||||
|
||||
def get_check_kwargs(self, options):
|
||||
if self.requires_system_checks == ALL_CHECKS:
|
||||
return {}
|
||||
return {"tags": self.requires_system_checks}
|
||||
|
||||
def check(
|
||||
self,
|
||||
app_configs=None,
|
||||
tags=None,
|
||||
display_num_errors=False,
|
||||
include_deployment_checks=False,
|
||||
fail_level=checks.ERROR,
|
||||
databases=None,
|
||||
):
|
||||
"""
|
||||
Use the system check framework to validate entire Django project.
|
||||
Raise CommandError for any serious message (error or critical errors).
|
||||
If there are only light messages (like warnings), print them to stderr
|
||||
and don't raise an exception.
|
||||
"""
|
||||
all_issues = checks.run_checks(
|
||||
app_configs=app_configs,
|
||||
tags=tags,
|
||||
include_deployment_checks=include_deployment_checks,
|
||||
databases=databases,
|
||||
)
|
||||
|
||||
header, body, footer = "", "", ""
|
||||
visible_issue_count = 0 # excludes silenced warnings
|
||||
|
||||
if all_issues:
|
||||
debugs = [
|
||||
e for e in all_issues if e.level < checks.INFO and not e.is_silenced()
|
||||
]
|
||||
infos = [
|
||||
e
|
||||
for e in all_issues
|
||||
if checks.INFO <= e.level < checks.WARNING and not e.is_silenced()
|
||||
]
|
||||
warnings = [
|
||||
e
|
||||
for e in all_issues
|
||||
if checks.WARNING <= e.level < checks.ERROR and not e.is_silenced()
|
||||
]
|
||||
errors = [
|
||||
e
|
||||
for e in all_issues
|
||||
if checks.ERROR <= e.level < checks.CRITICAL and not e.is_silenced()
|
||||
]
|
||||
criticals = [
|
||||
e
|
||||
for e in all_issues
|
||||
if checks.CRITICAL <= e.level and not e.is_silenced()
|
||||
]
|
||||
sorted_issues = [
|
||||
(criticals, "CRITICALS"),
|
||||
(errors, "ERRORS"),
|
||||
(warnings, "WARNINGS"),
|
||||
(infos, "INFOS"),
|
||||
(debugs, "DEBUGS"),
|
||||
]
|
||||
|
||||
for issues, group_name in sorted_issues:
|
||||
if issues:
|
||||
visible_issue_count += len(issues)
|
||||
formatted = (
|
||||
(
|
||||
self.style.ERROR(str(e))
|
||||
if e.is_serious()
|
||||
else self.style.WARNING(str(e))
|
||||
)
|
||||
for e in issues
|
||||
)
|
||||
formatted = "\n".join(sorted(formatted))
|
||||
body += "\n%s:\n%s\n" % (group_name, formatted)
|
||||
|
||||
if visible_issue_count:
|
||||
header = "System check identified some issues:\n"
|
||||
|
||||
if display_num_errors:
|
||||
if visible_issue_count:
|
||||
footer += "\n"
|
||||
footer += "System check identified %s (%s silenced)." % (
|
||||
(
|
||||
"no issues"
|
||||
if visible_issue_count == 0
|
||||
else (
|
||||
"1 issue"
|
||||
if visible_issue_count == 1
|
||||
else "%s issues" % visible_issue_count
|
||||
)
|
||||
),
|
||||
len(all_issues) - visible_issue_count,
|
||||
)
|
||||
|
||||
if any(e.is_serious(fail_level) and not e.is_silenced() for e in all_issues):
|
||||
msg = self.style.ERROR("SystemCheckError: %s" % header) + body + footer
|
||||
raise SystemCheckError(msg)
|
||||
else:
|
||||
msg = header + body + footer
|
||||
|
||||
if msg:
|
||||
if visible_issue_count:
|
||||
self.stderr.write(msg, lambda x: x)
|
||||
else:
|
||||
self.stdout.write(msg)
|
||||
|
||||
def check_migrations(self):
|
||||
"""
|
||||
Print a warning if the set of migrations on disk don't match the
|
||||
migrations in the database.
|
||||
"""
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
|
||||
try:
|
||||
executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
|
||||
except ImproperlyConfigured:
|
||||
# No databases are configured (or the dummy one)
|
||||
return
|
||||
|
||||
plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
|
||||
if plan:
|
||||
apps_waiting_migration = sorted(
|
||||
{migration.app_label for migration, backwards in plan}
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.NOTICE(
|
||||
"\nYou have %(unapplied_migration_count)s unapplied migration(s). "
|
||||
"Your project may not work properly until you apply the "
|
||||
"migrations for app(s): %(apps_waiting_migration)s."
|
||||
% {
|
||||
"unapplied_migration_count": len(plan),
|
||||
"apps_waiting_migration": ", ".join(apps_waiting_migration),
|
||||
}
|
||||
)
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.NOTICE("Run 'python manage.py migrate' to apply them.")
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
The actual logic of the command. Subclasses must implement
|
||||
this method.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseCommand must provide a handle() method"
|
||||
)
|
||||
|
||||
|
||||
class AppCommand(BaseCommand):
|
||||
"""
|
||||
A management command which takes one or more installed application labels
|
||||
as arguments, and does something with each of them.
|
||||
|
||||
Rather than implementing ``handle()``, subclasses must implement
|
||||
``handle_app_config()``, which will be called once for each application.
|
||||
"""
|
||||
|
||||
missing_args_message = "Enter at least one application label."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"args",
|
||||
metavar="app_label",
|
||||
nargs="+",
|
||||
help="One or more application label.",
|
||||
)
|
||||
|
||||
def handle(self, *app_labels, **options):
|
||||
from django.apps import apps
|
||||
|
||||
try:
|
||||
app_configs = [apps.get_app_config(app_label) for app_label in app_labels]
|
||||
except (LookupError, ImportError) as e:
|
||||
raise CommandError(
|
||||
"%s. Are you sure your INSTALLED_APPS setting is correct?" % e
|
||||
)
|
||||
output = []
|
||||
for app_config in app_configs:
|
||||
app_output = self.handle_app_config(app_config, **options)
|
||||
if app_output:
|
||||
output.append(app_output)
|
||||
return "\n".join(output)
|
||||
|
||||
def handle_app_config(self, app_config, **options):
|
||||
"""
|
||||
Perform the command's actions for app_config, an AppConfig instance
|
||||
corresponding to an application label given on the command line.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Subclasses of AppCommand must provide a handle_app_config() method."
|
||||
)
|
||||
|
||||
|
||||
class LabelCommand(BaseCommand):
|
||||
"""
|
||||
A management command which takes one or more arbitrary arguments
|
||||
(labels) on the command line, and does something with each of
|
||||
them.
|
||||
|
||||
Rather than implementing ``handle()``, subclasses must implement
|
||||
``handle_label()``, which will be called once for each label.
|
||||
|
||||
If the arguments should be names of installed applications, use
|
||||
``AppCommand`` instead.
|
||||
"""
|
||||
|
||||
label = "label"
|
||||
missing_args_message = "Enter at least one %s."
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.missing_args_message == LabelCommand.missing_args_message:
|
||||
self.missing_args_message = self.missing_args_message % self.label
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("args", metavar=self.label, nargs="+")
|
||||
|
||||
def handle(self, *labels, **options):
|
||||
output = []
|
||||
for label in labels:
|
||||
label_output = self.handle_label(label, **options)
|
||||
if label_output:
|
||||
output.append(label_output)
|
||||
return "\n".join(output)
|
||||
|
||||
def handle_label(self, label, **options):
|
||||
"""
|
||||
Perform the command's actions for ``label``, which will be the
|
||||
string as given on the command line.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of LabelCommand must provide a handle_label() method"
|
||||
)
|
||||
120
.venv/Lib/site-packages/django/core/management/color.py
Normal file
120
.venv/Lib/site-packages/django/core/management/color.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Sets up the terminal color scheme.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.utils import termcolors
|
||||
|
||||
try:
|
||||
import colorama
|
||||
|
||||
# Avoid initializing colorama in non-Windows platforms.
|
||||
colorama.just_fix_windows_console()
|
||||
except (
|
||||
AttributeError, # colorama <= 0.4.6.
|
||||
ImportError, # colorama is not installed.
|
||||
# If just_fix_windows_console() accesses sys.stdout with
|
||||
# WSGIRestrictedStdout.
|
||||
OSError,
|
||||
):
|
||||
HAS_COLORAMA = False
|
||||
else:
|
||||
HAS_COLORAMA = True
|
||||
|
||||
|
||||
def supports_color():
|
||||
"""
|
||||
Return True if the running system's terminal supports color,
|
||||
and False otherwise.
|
||||
"""
|
||||
|
||||
def vt_codes_enabled_in_windows_registry():
|
||||
"""
|
||||
Check the Windows Registry to see if VT code handling has been enabled
|
||||
by default, see https://superuser.com/a/1300251/447564.
|
||||
"""
|
||||
try:
|
||||
# winreg is only available on Windows.
|
||||
import winreg
|
||||
except ImportError:
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
reg_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Console")
|
||||
reg_key_value, _ = winreg.QueryValueEx(reg_key, "VirtualTerminalLevel")
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
else:
|
||||
return reg_key_value == 1
|
||||
|
||||
# isatty is not always implemented, #6223.
|
||||
is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
||||
|
||||
return is_a_tty and (
|
||||
sys.platform != "win32"
|
||||
or (HAS_COLORAMA and getattr(colorama, "fixed_windows_console", False))
|
||||
or "ANSICON" in os.environ
|
||||
or
|
||||
# Windows Terminal supports VT codes.
|
||||
"WT_SESSION" in os.environ
|
||||
or
|
||||
# Microsoft Visual Studio Code's built-in terminal supports colors.
|
||||
os.environ.get("TERM_PROGRAM") == "vscode"
|
||||
or vt_codes_enabled_in_windows_registry()
|
||||
)
|
||||
|
||||
|
||||
class Style:
|
||||
pass
|
||||
|
||||
|
||||
def make_style(config_string=""):
|
||||
"""
|
||||
Create a Style object from the given config_string.
|
||||
|
||||
If config_string is empty django.utils.termcolors.DEFAULT_PALETTE is used.
|
||||
"""
|
||||
|
||||
style = Style()
|
||||
|
||||
color_settings = termcolors.parse_color_setting(config_string)
|
||||
|
||||
# The nocolor palette has all available roles.
|
||||
# Use that palette as the basis for populating
|
||||
# the palette as defined in the environment.
|
||||
for role in termcolors.PALETTES[termcolors.NOCOLOR_PALETTE]:
|
||||
if color_settings:
|
||||
format = color_settings.get(role, {})
|
||||
style_func = termcolors.make_style(**format)
|
||||
else:
|
||||
|
||||
def style_func(x):
|
||||
return x
|
||||
|
||||
setattr(style, role, style_func)
|
||||
|
||||
# For backwards compatibility,
|
||||
# set style for ERROR_OUTPUT == ERROR
|
||||
style.ERROR_OUTPUT = style.ERROR
|
||||
|
||||
return style
|
||||
|
||||
|
||||
@functools.cache
|
||||
def no_style():
|
||||
"""
|
||||
Return a Style object with no color scheme.
|
||||
"""
|
||||
return make_style("nocolor")
|
||||
|
||||
|
||||
def color_style(force_color=False):
|
||||
"""
|
||||
Return a Style object from the Django color scheme.
|
||||
"""
|
||||
if not force_color and not supports_color():
|
||||
return no_style()
|
||||
return make_style(os.environ.get("DJANGO_COLORS", ""))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,88 @@
|
||||
from django.apps import apps
|
||||
from django.core import checks
|
||||
from django.core.checks.registry import registry
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import connections
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Checks the entire Django project for potential problems."
|
||||
|
||||
requires_system_checks = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("args", metavar="app_label", nargs="*")
|
||||
parser.add_argument(
|
||||
"--tag",
|
||||
"-t",
|
||||
action="append",
|
||||
dest="tags",
|
||||
help="Run only checks labeled with given tag.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-tags",
|
||||
action="store_true",
|
||||
help=(
|
||||
"List available tags. Specify --deploy to include available deployment "
|
||||
"tags."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deploy",
|
||||
action="store_true",
|
||||
help="Check deployment settings.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fail-level",
|
||||
default="ERROR",
|
||||
choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
help=(
|
||||
"Message level that will cause the command to exit with a "
|
||||
"non-zero status. Default is ERROR."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
action="append",
|
||||
choices=tuple(connections),
|
||||
dest="databases",
|
||||
help="Run database related checks against these aliases.",
|
||||
)
|
||||
|
||||
def handle(self, *app_labels, **options):
|
||||
include_deployment_checks = options["deploy"]
|
||||
if options["list_tags"]:
|
||||
self.stdout.write(
|
||||
"\n".join(sorted(registry.tags_available(include_deployment_checks)))
|
||||
)
|
||||
return
|
||||
|
||||
if app_labels:
|
||||
app_configs = [apps.get_app_config(app_label) for app_label in app_labels]
|
||||
else:
|
||||
app_configs = None
|
||||
|
||||
tags = options["tags"]
|
||||
if tags:
|
||||
try:
|
||||
invalid_tag = next(
|
||||
tag
|
||||
for tag in tags
|
||||
if not checks.tag_exists(tag, include_deployment_checks)
|
||||
)
|
||||
except StopIteration:
|
||||
# no invalid tags
|
||||
pass
|
||||
else:
|
||||
raise CommandError(
|
||||
'There is no system check with the "%s" tag.' % invalid_tag
|
||||
)
|
||||
|
||||
self.check(
|
||||
app_configs=app_configs,
|
||||
tags=tags,
|
||||
display_num_errors=True,
|
||||
include_deployment_checks=include_deployment_checks,
|
||||
fail_level=getattr(checks, options["fail_level"]),
|
||||
databases=options["databases"],
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
import codecs
|
||||
import concurrent.futures
|
||||
import glob
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.utils import find_command, is_ignored_path, popen_wrapper
|
||||
|
||||
|
||||
def has_bom(fn):
|
||||
with fn.open("rb") as f:
|
||||
sample = f.read(4)
|
||||
return sample.startswith(
|
||||
(codecs.BOM_UTF8, codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)
|
||||
)
|
||||
|
||||
|
||||
def is_dir_writable(path):
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(dir=path):
|
||||
pass
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Compiles .po files to .mo files for use with builtin gettext support."
|
||||
|
||||
requires_system_checks = []
|
||||
|
||||
program = "msgfmt"
|
||||
program_options = ["--check-format"]
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--locale",
|
||||
"-l",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Locale(s) to process (e.g. de_AT). Default is to process all. "
|
||||
"Can be used multiple times.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exclude",
|
||||
"-x",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Locales to exclude. Default is none. Can be used multiple times.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-fuzzy",
|
||||
"-f",
|
||||
dest="fuzzy",
|
||||
action="store_true",
|
||||
help="Use fuzzy translations.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore",
|
||||
"-i",
|
||||
action="append",
|
||||
dest="ignore_patterns",
|
||||
default=[],
|
||||
metavar="PATTERN",
|
||||
help="Ignore directories matching this glob-style pattern. "
|
||||
"Use multiple times to ignore more.",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
locale = options["locale"]
|
||||
exclude = options["exclude"]
|
||||
ignore_patterns = set(options["ignore_patterns"])
|
||||
self.verbosity = options["verbosity"]
|
||||
if options["fuzzy"]:
|
||||
self.program_options = self.program_options + ["-f"]
|
||||
|
||||
if find_command(self.program) is None:
|
||||
raise CommandError(
|
||||
f"Can't find {self.program}. Make sure you have GNU gettext "
|
||||
"tools 0.19 or newer installed."
|
||||
)
|
||||
|
||||
basedirs = [os.path.join("conf", "locale"), "locale"]
|
||||
if os.environ.get("DJANGO_SETTINGS_MODULE"):
|
||||
from django.conf import settings
|
||||
|
||||
basedirs.extend(settings.LOCALE_PATHS)
|
||||
|
||||
# Walk entire tree, looking for locale directories
|
||||
for dirpath, dirnames, filenames in os.walk(".", topdown=True):
|
||||
# As we may modify dirnames, iterate through a copy of it instead
|
||||
for dirname in list(dirnames):
|
||||
if is_ignored_path(
|
||||
os.path.normpath(os.path.join(dirpath, dirname)), ignore_patterns
|
||||
):
|
||||
dirnames.remove(dirname)
|
||||
elif dirname == "locale":
|
||||
basedirs.append(os.path.join(dirpath, dirname))
|
||||
|
||||
# Gather existing directories.
|
||||
basedirs = set(map(os.path.abspath, filter(os.path.isdir, basedirs)))
|
||||
|
||||
if not basedirs:
|
||||
raise CommandError(
|
||||
"This script should be run from the Django Git "
|
||||
"checkout or your project or app tree, or with "
|
||||
"the settings module specified."
|
||||
)
|
||||
|
||||
# Build locale list
|
||||
all_locales = []
|
||||
for basedir in basedirs:
|
||||
locale_dirs = filter(os.path.isdir, glob.glob("%s/*" % basedir))
|
||||
all_locales.extend(map(os.path.basename, locale_dirs))
|
||||
|
||||
# Account for excluded locales
|
||||
locales = locale or all_locales
|
||||
locales = set(locales).difference(exclude)
|
||||
|
||||
self.has_errors = False
|
||||
for basedir in basedirs:
|
||||
if locales:
|
||||
dirs = [
|
||||
os.path.join(basedir, locale, "LC_MESSAGES") for locale in locales
|
||||
]
|
||||
else:
|
||||
dirs = [basedir]
|
||||
locations = []
|
||||
for ldir in dirs:
|
||||
for dirpath, dirnames, filenames in os.walk(ldir):
|
||||
locations.extend(
|
||||
(dirpath, f) for f in filenames if f.endswith(".po")
|
||||
)
|
||||
if locations:
|
||||
self.compile_messages(locations)
|
||||
|
||||
if self.has_errors:
|
||||
raise CommandError("compilemessages generated one or more errors.")
|
||||
|
||||
def compile_messages(self, locations):
|
||||
"""
|
||||
Locations is a list of tuples: [(directory, file), ...]
|
||||
"""
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = []
|
||||
for i, (dirpath, f) in enumerate(locations):
|
||||
po_path = Path(dirpath) / f
|
||||
mo_path = po_path.with_suffix(".mo")
|
||||
try:
|
||||
if mo_path.stat().st_mtime >= po_path.stat().st_mtime:
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write(
|
||||
"File “%s” is already compiled and up to date."
|
||||
% po_path
|
||||
)
|
||||
continue
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write("processing file %s in %s" % (f, dirpath))
|
||||
|
||||
if has_bom(po_path):
|
||||
self.stderr.write(
|
||||
"The %s file has a BOM (Byte Order Mark). Django only "
|
||||
"supports .po files encoded in UTF-8 and without any BOM."
|
||||
% po_path
|
||||
)
|
||||
self.has_errors = True
|
||||
continue
|
||||
|
||||
# Check writability on first location
|
||||
if i == 0 and not is_dir_writable(mo_path.parent):
|
||||
self.stderr.write(
|
||||
"The po files under %s are in a seemingly not writable "
|
||||
"location. mo files will not be updated/created." % dirpath
|
||||
)
|
||||
self.has_errors = True
|
||||
return
|
||||
|
||||
args = [self.program, *self.program_options, "-o", mo_path, po_path]
|
||||
futures.append(executor.submit(popen_wrapper, args))
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
output, errors, status = future.result()
|
||||
if status:
|
||||
if self.verbosity > 0:
|
||||
if errors:
|
||||
self.stderr.write(
|
||||
"Execution of %s failed: %s" % (self.program, errors)
|
||||
)
|
||||
else:
|
||||
self.stderr.write("Execution of %s failed" % self.program)
|
||||
self.has_errors = True
|
||||
@@ -0,0 +1,131 @@
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches
|
||||
from django.core.cache.backends.db import BaseDatabaseCache
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import (
|
||||
DEFAULT_DB_ALIAS,
|
||||
DatabaseError,
|
||||
connections,
|
||||
models,
|
||||
router,
|
||||
transaction,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates the tables needed to use the SQL cache backend."
|
||||
|
||||
requires_system_checks = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"args",
|
||||
metavar="table_name",
|
||||
nargs="*",
|
||||
help=(
|
||||
"Optional table names. Otherwise, settings.CACHES is used to find "
|
||||
"cache tables."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help="Nominates a database onto which the cache tables will be "
|
||||
'installed. Defaults to the "default" database.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Does not create the table, just prints the SQL that would be run.",
|
||||
)
|
||||
|
||||
def handle(self, *tablenames, **options):
|
||||
db = options["database"]
|
||||
self.verbosity = options["verbosity"]
|
||||
dry_run = options["dry_run"]
|
||||
if tablenames:
|
||||
# Legacy behavior, tablename specified as argument
|
||||
for tablename in tablenames:
|
||||
self.create_table(db, tablename, dry_run)
|
||||
else:
|
||||
for cache_alias in settings.CACHES:
|
||||
cache = caches[cache_alias]
|
||||
if isinstance(cache, BaseDatabaseCache):
|
||||
self.create_table(db, cache._table, dry_run)
|
||||
|
||||
def create_table(self, database, tablename, dry_run):
|
||||
cache = BaseDatabaseCache(tablename, {})
|
||||
if not router.allow_migrate_model(database, cache.cache_model_class):
|
||||
return
|
||||
connection = connections[database]
|
||||
|
||||
if tablename in connection.introspection.table_names():
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write("Cache table '%s' already exists." % tablename)
|
||||
return
|
||||
|
||||
fields = (
|
||||
# "key" is a reserved word in MySQL, so use "cache_key" instead.
|
||||
models.CharField(
|
||||
name="cache_key", max_length=255, unique=True, primary_key=True
|
||||
),
|
||||
models.TextField(name="value"),
|
||||
models.DateTimeField(name="expires", db_index=True),
|
||||
)
|
||||
table_output = []
|
||||
index_output = []
|
||||
qn = connection.ops.quote_name
|
||||
for f in fields:
|
||||
field_output = [
|
||||
qn(f.name),
|
||||
f.db_type(connection=connection),
|
||||
"%sNULL" % ("NOT " if not f.null else ""),
|
||||
]
|
||||
if f.primary_key:
|
||||
field_output.append("PRIMARY KEY")
|
||||
elif f.unique:
|
||||
field_output.append("UNIQUE")
|
||||
if f.db_index:
|
||||
unique = "UNIQUE " if f.unique else ""
|
||||
index_output.append(
|
||||
"CREATE %sINDEX %s ON %s (%s);"
|
||||
% (
|
||||
unique,
|
||||
qn("%s_%s" % (tablename, f.name)),
|
||||
qn(tablename),
|
||||
qn(f.name),
|
||||
)
|
||||
)
|
||||
table_output.append(" ".join(field_output))
|
||||
full_statement = ["CREATE TABLE %s (" % qn(tablename)]
|
||||
for i, line in enumerate(table_output):
|
||||
full_statement.append(
|
||||
" %s%s" % (line, "," if i < len(table_output) - 1 else "")
|
||||
)
|
||||
full_statement.append(");")
|
||||
|
||||
full_statement = "\n".join(full_statement)
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(full_statement)
|
||||
for statement in index_output:
|
||||
self.stdout.write(statement)
|
||||
return
|
||||
|
||||
with transaction.atomic(
|
||||
using=database, savepoint=connection.features.can_rollback_ddl
|
||||
):
|
||||
with connection.cursor() as curs:
|
||||
try:
|
||||
curs.execute(full_statement)
|
||||
except DatabaseError as e:
|
||||
raise CommandError(
|
||||
"Cache table '%s' could not be created.\nThe error was: %s."
|
||||
% (tablename, e)
|
||||
)
|
||||
for statement in index_output:
|
||||
curs.execute(statement)
|
||||
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write("Cache table '%s' created." % tablename)
|
||||
@@ -0,0 +1,49 @@
|
||||
import subprocess
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Runs the command-line client for specified database, or the "
|
||||
"default database if none is provided."
|
||||
)
|
||||
|
||||
requires_system_checks = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help=(
|
||||
"Nominates a database onto which to open a shell. Defaults to the "
|
||||
'"default" database.'
|
||||
),
|
||||
)
|
||||
parameters = parser.add_argument_group("parameters")
|
||||
parameters.add_argument("parameters", nargs="*")
|
||||
|
||||
def handle(self, **options):
|
||||
connection = connections[options["database"]]
|
||||
try:
|
||||
connection.client.runshell(options["parameters"])
|
||||
except FileNotFoundError:
|
||||
# Note that we're assuming the FileNotFoundError relates to the
|
||||
# command missing. It could be raised for some other reason, in
|
||||
# which case this error message would be inaccurate. Still, this
|
||||
# message catches the common case.
|
||||
raise CommandError(
|
||||
"You appear not to have the %r program installed or on your path."
|
||||
% connection.client.executable_name
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise CommandError(
|
||||
'"%s" returned non-zero exit status %s.'
|
||||
% (
|
||||
" ".join(map(str, e.cmd)),
|
||||
e.returncode,
|
||||
),
|
||||
returncode=e.returncode,
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
def module_to_dict(module, omittable=lambda k: k.startswith("_") or not k.isupper()):
|
||||
"""Convert a module namespace to a Python dictionary."""
|
||||
return {k: repr(getattr(module, k)) for k in dir(module) if not omittable(k)}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Displays differences between the current settings.py and Django's
|
||||
default settings."""
|
||||
|
||||
requires_system_checks = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help=(
|
||||
'Display all settings, regardless of their value. In "hash" '
|
||||
'mode, default values are prefixed by "###".'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--default",
|
||||
metavar="MODULE",
|
||||
help=(
|
||||
"The settings module to compare the current settings against. Leave "
|
||||
"empty to compare against Django's default settings."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default="hash",
|
||||
choices=("hash", "unified"),
|
||||
help=(
|
||||
"Selects the output format. 'hash' mode displays each changed "
|
||||
"setting, with the settings that don't appear in the defaults "
|
||||
"followed by ###. 'unified' mode prefixes the default setting "
|
||||
"with a minus sign, followed by the changed setting prefixed "
|
||||
"with a plus sign."
|
||||
),
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
from django.conf import Settings, global_settings, settings
|
||||
|
||||
# Because settings are imported lazily, we need to explicitly load them.
|
||||
if not settings.configured:
|
||||
settings._setup()
|
||||
|
||||
user_settings = module_to_dict(settings._wrapped)
|
||||
default = options["default"]
|
||||
default_settings = module_to_dict(
|
||||
Settings(default) if default else global_settings
|
||||
)
|
||||
output_func = {
|
||||
"hash": self.output_hash,
|
||||
"unified": self.output_unified,
|
||||
}[options["output"]]
|
||||
return "\n".join(output_func(user_settings, default_settings, **options))
|
||||
|
||||
def output_hash(self, user_settings, default_settings, **options):
|
||||
# Inspired by Postfix's "postconf -n".
|
||||
output = []
|
||||
for key in sorted(user_settings):
|
||||
if key not in default_settings:
|
||||
output.append("%s = %s ###" % (key, user_settings[key]))
|
||||
elif user_settings[key] != default_settings[key]:
|
||||
output.append("%s = %s" % (key, user_settings[key]))
|
||||
elif options["all"]:
|
||||
output.append("### %s = %s" % (key, user_settings[key]))
|
||||
return output
|
||||
|
||||
def output_unified(self, user_settings, default_settings, **options):
|
||||
output = []
|
||||
for key in sorted(user_settings):
|
||||
if key not in default_settings:
|
||||
output.append(
|
||||
self.style.SUCCESS("+ %s = %s" % (key, user_settings[key]))
|
||||
)
|
||||
elif user_settings[key] != default_settings[key]:
|
||||
output.append(
|
||||
self.style.ERROR("- %s = %s" % (key, default_settings[key]))
|
||||
)
|
||||
output.append(
|
||||
self.style.SUCCESS("+ %s = %s" % (key, user_settings[key]))
|
||||
)
|
||||
elif options["all"]:
|
||||
output.append(" %s = %s" % (key, user_settings[key]))
|
||||
return output
|
||||
@@ -0,0 +1,285 @@
|
||||
import gzip
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from django.apps import apps
|
||||
from django.core import serializers
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.utils import parse_apps_and_model_labels
|
||||
from django.db import DEFAULT_DB_ALIAS, connections, router
|
||||
|
||||
try:
|
||||
import bz2
|
||||
|
||||
has_bz2 = True
|
||||
except ImportError:
|
||||
has_bz2 = False
|
||||
|
||||
try:
|
||||
import lzma
|
||||
|
||||
has_lzma = True
|
||||
except ImportError:
|
||||
has_lzma = False
|
||||
|
||||
|
||||
class ProxyModelWarning(Warning):
|
||||
pass
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Output the contents of the database as a fixture of the given format "
|
||||
"(using each model's default manager unless --all is specified)."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"args",
|
||||
metavar="app_label[.ModelName]",
|
||||
nargs="*",
|
||||
help=(
|
||||
"Restricts dumped data to the specified app_label or "
|
||||
"app_label.ModelName."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
default="json",
|
||||
help="Specifies the output serialization format for fixtures.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--indent",
|
||||
type=int,
|
||||
help="Specifies the indent level to use when pretty-printing output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help="Nominates a specific database to dump fixtures from. "
|
||||
'Defaults to the "default" database.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--exclude",
|
||||
action="append",
|
||||
default=[],
|
||||
help="An app_label or app_label.ModelName to exclude "
|
||||
"(use multiple --exclude to exclude multiple apps/models).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--natural-foreign",
|
||||
action="store_true",
|
||||
dest="use_natural_foreign_keys",
|
||||
help="Use natural foreign keys if they are available.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--natural-primary",
|
||||
action="store_true",
|
||||
dest="use_natural_primary_keys",
|
||||
help="Use natural primary keys if they are available.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--all",
|
||||
action="store_true",
|
||||
dest="use_base_manager",
|
||||
help=(
|
||||
"Use Django's base manager to dump all models stored in the database, "
|
||||
"including those that would otherwise be filtered or modified by a "
|
||||
"custom manager."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pks",
|
||||
dest="primary_keys",
|
||||
help="Only dump objects with given primary keys. Accepts a comma-separated "
|
||||
"list of keys. This option only works when you specify one model.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--output", help="Specifies file to which the output is written."
|
||||
)
|
||||
|
||||
def handle(self, *app_labels, **options):
|
||||
format = options["format"]
|
||||
indent = options["indent"]
|
||||
using = options["database"]
|
||||
excludes = options["exclude"]
|
||||
output = options["output"]
|
||||
show_traceback = options["traceback"]
|
||||
use_natural_foreign_keys = options["use_natural_foreign_keys"]
|
||||
use_natural_primary_keys = options["use_natural_primary_keys"]
|
||||
use_base_manager = options["use_base_manager"]
|
||||
pks = options["primary_keys"]
|
||||
|
||||
if pks:
|
||||
primary_keys = [pk.strip() for pk in pks.split(",")]
|
||||
else:
|
||||
primary_keys = []
|
||||
|
||||
excluded_models, excluded_apps = parse_apps_and_model_labels(excludes)
|
||||
|
||||
if not app_labels:
|
||||
if primary_keys:
|
||||
raise CommandError("You can only use --pks option with one model")
|
||||
app_list = dict.fromkeys(
|
||||
app_config
|
||||
for app_config in apps.get_app_configs()
|
||||
if app_config.models_module is not None
|
||||
and app_config not in excluded_apps
|
||||
)
|
||||
else:
|
||||
if len(app_labels) > 1 and primary_keys:
|
||||
raise CommandError("You can only use --pks option with one model")
|
||||
app_list = {}
|
||||
for label in app_labels:
|
||||
try:
|
||||
app_label, model_label = label.split(".")
|
||||
try:
|
||||
app_config = apps.get_app_config(app_label)
|
||||
except LookupError as e:
|
||||
raise CommandError(str(e))
|
||||
if app_config.models_module is None or app_config in excluded_apps:
|
||||
continue
|
||||
try:
|
||||
model = app_config.get_model(model_label)
|
||||
except LookupError:
|
||||
raise CommandError(
|
||||
"Unknown model: %s.%s" % (app_label, model_label)
|
||||
)
|
||||
|
||||
app_list_value = app_list.setdefault(app_config, [])
|
||||
|
||||
# We may have previously seen an "all-models" request for
|
||||
# this app (no model qualifier was given). In this case
|
||||
# there is no need adding specific models to the list.
|
||||
if app_list_value is not None and model not in app_list_value:
|
||||
app_list_value.append(model)
|
||||
except ValueError:
|
||||
if primary_keys:
|
||||
raise CommandError(
|
||||
"You can only use --pks option with one model"
|
||||
)
|
||||
# This is just an app - no model qualifier
|
||||
app_label = label
|
||||
try:
|
||||
app_config = apps.get_app_config(app_label)
|
||||
except LookupError as e:
|
||||
raise CommandError(str(e))
|
||||
if app_config.models_module is None or app_config in excluded_apps:
|
||||
continue
|
||||
app_list[app_config] = None
|
||||
|
||||
# Check that the serialization format exists; this is a shortcut to
|
||||
# avoid collating all the objects and _then_ failing.
|
||||
if format not in serializers.get_public_serializer_formats():
|
||||
try:
|
||||
serializers.get_serializer(format)
|
||||
except serializers.SerializerDoesNotExist:
|
||||
pass
|
||||
|
||||
raise CommandError("Unknown serialization format: %s" % format)
|
||||
|
||||
def get_objects(count_only=False):
|
||||
"""
|
||||
Collate the objects to be serialized. If count_only is True, just
|
||||
count the number of objects to be serialized.
|
||||
"""
|
||||
if use_natural_foreign_keys:
|
||||
models = serializers.sort_dependencies(
|
||||
app_list.items(), allow_cycles=True
|
||||
)
|
||||
else:
|
||||
# There is no need to sort dependencies when natural foreign
|
||||
# keys are not used.
|
||||
models = []
|
||||
for app_config, model_list in app_list.items():
|
||||
if model_list is None:
|
||||
models.extend(app_config.get_models())
|
||||
else:
|
||||
models.extend(model_list)
|
||||
for model in models:
|
||||
if model in excluded_models:
|
||||
continue
|
||||
if model._meta.proxy and model._meta.proxy_for_model not in models:
|
||||
warnings.warn(
|
||||
"%s is a proxy model and won't be serialized."
|
||||
% model._meta.label,
|
||||
category=ProxyModelWarning,
|
||||
)
|
||||
if not model._meta.proxy and router.allow_migrate_model(using, model):
|
||||
if use_base_manager:
|
||||
objects = model._base_manager
|
||||
else:
|
||||
objects = model._default_manager
|
||||
|
||||
queryset = objects.using(using).order_by(model._meta.pk.name)
|
||||
if primary_keys:
|
||||
queryset = queryset.filter(pk__in=primary_keys)
|
||||
if count_only:
|
||||
yield queryset.order_by().count()
|
||||
else:
|
||||
chunk_size = (
|
||||
2000 if queryset._prefetch_related_lookups else None
|
||||
)
|
||||
yield from queryset.iterator(chunk_size=chunk_size)
|
||||
|
||||
try:
|
||||
self.stdout.ending = None
|
||||
progress_output = None
|
||||
object_count = 0
|
||||
# If dumpdata is outputting to stdout, there is no way to display progress
|
||||
if output and self.stdout.isatty() and options["verbosity"] > 0:
|
||||
progress_output = self.stdout
|
||||
object_count = sum(get_objects(count_only=True))
|
||||
if output:
|
||||
file_root, file_ext = os.path.splitext(output)
|
||||
compression_formats = {
|
||||
".bz2": (open, {}, file_root),
|
||||
".gz": (gzip.open, {}, output),
|
||||
".lzma": (open, {}, file_root),
|
||||
".xz": (open, {}, file_root),
|
||||
".zip": (open, {}, file_root),
|
||||
}
|
||||
if has_bz2:
|
||||
compression_formats[".bz2"] = (bz2.open, {}, output)
|
||||
if has_lzma:
|
||||
compression_formats[".lzma"] = (
|
||||
lzma.open,
|
||||
{"format": lzma.FORMAT_ALONE},
|
||||
output,
|
||||
)
|
||||
compression_formats[".xz"] = (lzma.open, {}, output)
|
||||
try:
|
||||
open_method, kwargs, file_path = compression_formats[file_ext]
|
||||
except KeyError:
|
||||
open_method, kwargs, file_path = (open, {}, output)
|
||||
if file_path != output:
|
||||
file_name = os.path.basename(file_path)
|
||||
warnings.warn(
|
||||
f"Unsupported file extension ({file_ext}). "
|
||||
f"Fixtures saved in '{file_name}'.",
|
||||
RuntimeWarning,
|
||||
)
|
||||
stream = open_method(file_path, "wt", **kwargs)
|
||||
else:
|
||||
stream = None
|
||||
try:
|
||||
serializers.serialize(
|
||||
format,
|
||||
get_objects(),
|
||||
indent=indent,
|
||||
use_natural_foreign_keys=use_natural_foreign_keys,
|
||||
use_natural_primary_keys=use_natural_primary_keys,
|
||||
stream=stream or self.stdout,
|
||||
progress_output=progress_output,
|
||||
object_count=object_count,
|
||||
)
|
||||
finally:
|
||||
if stream:
|
||||
stream.close()
|
||||
except Exception as e:
|
||||
if show_traceback:
|
||||
raise
|
||||
raise CommandError("Unable to serialize database: %s" % e)
|
||||
@@ -0,0 +1,93 @@
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.color import no_style
|
||||
from django.core.management.sql import emit_post_migrate_signal, sql_flush
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Removes ALL DATA from the database, including data added during "
|
||||
'migrations. Does not achieve a "fresh install" state.'
|
||||
)
|
||||
stealth_options = ("reset_sequences", "allow_cascade", "inhibit_post_migrate")
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
help="Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help='Nominates a database to flush. Defaults to the "default" database.',
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
database = options["database"]
|
||||
connection = connections[database]
|
||||
verbosity = options["verbosity"]
|
||||
interactive = options["interactive"]
|
||||
# The following are stealth options used by Django's internals.
|
||||
reset_sequences = options.get("reset_sequences", True)
|
||||
allow_cascade = options.get("allow_cascade", False)
|
||||
inhibit_post_migrate = options.get("inhibit_post_migrate", False)
|
||||
|
||||
self.style = no_style()
|
||||
|
||||
# Import the 'management' module within each installed app, to register
|
||||
# dispatcher events.
|
||||
for app_config in apps.get_app_configs():
|
||||
try:
|
||||
import_module(".management", app_config.name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
sql_list = sql_flush(
|
||||
self.style,
|
||||
connection,
|
||||
reset_sequences=reset_sequences,
|
||||
allow_cascade=allow_cascade,
|
||||
)
|
||||
|
||||
if interactive:
|
||||
confirm = input(
|
||||
"""You have requested a flush of the database.
|
||||
This will IRREVERSIBLY DESTROY all data currently in the "%s" database,
|
||||
and return each table to an empty state.
|
||||
Are you sure you want to do this?
|
||||
|
||||
Type 'yes' to continue, or 'no' to cancel: """
|
||||
% connection.settings_dict["NAME"]
|
||||
)
|
||||
else:
|
||||
confirm = "yes"
|
||||
|
||||
if confirm == "yes":
|
||||
try:
|
||||
connection.ops.execute_sql_flush(sql_list)
|
||||
except Exception as exc:
|
||||
raise CommandError(
|
||||
"Database %s couldn't be flushed. Possible reasons:\n"
|
||||
" * The database isn't running or isn't configured correctly.\n"
|
||||
" * At least one of the expected database tables doesn't exist.\n"
|
||||
" * The SQL was invalid.\n"
|
||||
"Hint: Look at the output of 'django-admin sqlflush'. "
|
||||
"That's the SQL this command wasn't able to run."
|
||||
% (connection.settings_dict["NAME"],)
|
||||
) from exc
|
||||
|
||||
# Empty sql_list may signify an empty database and post_migrate
|
||||
# would then crash.
|
||||
if sql_list and not inhibit_post_migrate:
|
||||
# Emit the post migrate signal. This allows individual applications to
|
||||
# respond as if the database had been migrated from scratch.
|
||||
emit_post_migrate_signal(verbosity, interactive, database)
|
||||
else:
|
||||
self.stdout.write("Flush cancelled.")
|
||||
@@ -0,0 +1,417 @@
|
||||
import keyword
|
||||
import re
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Introspects the database tables in the given database and outputs a Django "
|
||||
"model module."
|
||||
)
|
||||
requires_system_checks = []
|
||||
stealth_options = ("table_name_filter",)
|
||||
db_module = "django.db"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"table",
|
||||
nargs="*",
|
||||
type=str,
|
||||
help="Selects what tables or views should be introspected.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help=(
|
||||
'Nominates a database to introspect. Defaults to using the "default" '
|
||||
"database."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-partitions",
|
||||
action="store_true",
|
||||
help="Also output models for partition tables.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-views",
|
||||
action="store_true",
|
||||
help="Also output models for database views.",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
try:
|
||||
for line in self.handle_inspection(options):
|
||||
self.stdout.write(line)
|
||||
except NotImplementedError:
|
||||
raise CommandError(
|
||||
"Database inspection isn't supported for the currently selected "
|
||||
"database backend."
|
||||
)
|
||||
|
||||
def handle_inspection(self, options):
|
||||
connection = connections[options["database"]]
|
||||
# 'table_name_filter' is a stealth option
|
||||
table_name_filter = options.get("table_name_filter")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
yield "# This is an auto-generated Django model module."
|
||||
yield "# You'll have to do the following manually to clean this up:"
|
||||
yield "# * Rearrange models' order"
|
||||
yield "# * Make sure each model has one field with primary_key=True"
|
||||
yield (
|
||||
"# * Make sure each ForeignKey and OneToOneField has `on_delete` set "
|
||||
"to the desired behavior"
|
||||
)
|
||||
yield (
|
||||
"# * Remove `managed = False` lines if you wish to allow "
|
||||
"Django to create, modify, and delete the table"
|
||||
)
|
||||
yield (
|
||||
"# Feel free to rename the models, but don't rename db_table values or "
|
||||
"field names."
|
||||
)
|
||||
yield "from %s import models" % self.db_module
|
||||
known_models = []
|
||||
# Determine types of tables and/or views to be introspected.
|
||||
types = {"t"}
|
||||
if options["include_partitions"]:
|
||||
types.add("p")
|
||||
if options["include_views"]:
|
||||
types.add("v")
|
||||
table_info = connection.introspection.get_table_list(cursor)
|
||||
table_info = {info.name: info for info in table_info if info.type in types}
|
||||
|
||||
for table_name in options["table"] or sorted(name for name in table_info):
|
||||
if table_name_filter is not None and callable(table_name_filter):
|
||||
if not table_name_filter(table_name):
|
||||
continue
|
||||
try:
|
||||
try:
|
||||
relations = connection.introspection.get_relations(
|
||||
cursor, table_name
|
||||
)
|
||||
except NotImplementedError:
|
||||
relations = {}
|
||||
try:
|
||||
constraints = connection.introspection.get_constraints(
|
||||
cursor, table_name
|
||||
)
|
||||
except NotImplementedError:
|
||||
constraints = {}
|
||||
primary_key_columns = (
|
||||
connection.introspection.get_primary_key_columns(
|
||||
cursor, table_name
|
||||
)
|
||||
or []
|
||||
)
|
||||
primary_key_column = (
|
||||
primary_key_columns[0]
|
||||
if len(primary_key_columns) == 1
|
||||
else None
|
||||
)
|
||||
unique_columns = [
|
||||
c["columns"][0]
|
||||
for c in constraints.values()
|
||||
if c["unique"] and len(c["columns"]) == 1
|
||||
]
|
||||
table_description = connection.introspection.get_table_description(
|
||||
cursor, table_name
|
||||
)
|
||||
except Exception as e:
|
||||
yield "# Unable to inspect table '%s'" % table_name
|
||||
yield "# The error was: %s" % e
|
||||
continue
|
||||
|
||||
model_name = self.normalize_table_name(table_name)
|
||||
yield ""
|
||||
yield ""
|
||||
yield "class %s(models.Model):" % model_name
|
||||
known_models.append(model_name)
|
||||
|
||||
if len(primary_key_columns) > 1:
|
||||
fields = ", ".join([f"'{col}'" for col in primary_key_columns])
|
||||
yield f" pk = models.CompositePrimaryKey({fields})"
|
||||
|
||||
used_column_names = [] # Holds column names used in the table so far
|
||||
column_to_field_name = {} # Maps column names to names of model fields
|
||||
used_relations = set() # Holds foreign relations used in the table.
|
||||
for row in table_description:
|
||||
comment_notes = (
|
||||
[]
|
||||
) # Holds Field notes, to be displayed in a Python comment.
|
||||
extra_params = {} # Holds Field parameters such as 'db_column'.
|
||||
column_name = row.name
|
||||
is_relation = column_name in relations
|
||||
|
||||
att_name, params, notes = self.normalize_col_name(
|
||||
column_name, used_column_names, is_relation
|
||||
)
|
||||
extra_params.update(params)
|
||||
comment_notes.extend(notes)
|
||||
|
||||
used_column_names.append(att_name)
|
||||
column_to_field_name[column_name] = att_name
|
||||
|
||||
# Add primary_key and unique, if necessary.
|
||||
if column_name == primary_key_column:
|
||||
extra_params["primary_key"] = True
|
||||
elif column_name in unique_columns:
|
||||
extra_params["unique"] = True
|
||||
|
||||
if is_relation:
|
||||
ref_db_column, ref_db_table = relations[column_name]
|
||||
if extra_params.pop("unique", False) or extra_params.get(
|
||||
"primary_key"
|
||||
):
|
||||
rel_type = "OneToOneField"
|
||||
else:
|
||||
rel_type = "ForeignKey"
|
||||
ref_pk_column = (
|
||||
connection.introspection.get_primary_key_column(
|
||||
cursor, ref_db_table
|
||||
)
|
||||
)
|
||||
if ref_pk_column and ref_pk_column != ref_db_column:
|
||||
extra_params["to_field"] = ref_db_column
|
||||
rel_to = (
|
||||
"self"
|
||||
if ref_db_table == table_name
|
||||
else self.normalize_table_name(ref_db_table)
|
||||
)
|
||||
if rel_to in known_models:
|
||||
field_type = "%s(%s" % (rel_type, rel_to)
|
||||
else:
|
||||
field_type = "%s('%s'" % (rel_type, rel_to)
|
||||
if rel_to in used_relations:
|
||||
extra_params["related_name"] = "%s_%s_set" % (
|
||||
model_name.lower(),
|
||||
att_name,
|
||||
)
|
||||
used_relations.add(rel_to)
|
||||
else:
|
||||
# Calling `get_field_type` to get the field type string and any
|
||||
# additional parameters and notes.
|
||||
field_type, field_params, field_notes = self.get_field_type(
|
||||
connection, table_name, row
|
||||
)
|
||||
extra_params.update(field_params)
|
||||
comment_notes.extend(field_notes)
|
||||
|
||||
field_type += "("
|
||||
|
||||
# Don't output 'id = meta.AutoField(primary_key=True)', because
|
||||
# that's assumed if it doesn't exist.
|
||||
if att_name == "id" and extra_params == {"primary_key": True}:
|
||||
if field_type == "AutoField(":
|
||||
continue
|
||||
elif (
|
||||
field_type
|
||||
== connection.features.introspected_field_types["AutoField"]
|
||||
+ "("
|
||||
):
|
||||
comment_notes.append("AutoField?")
|
||||
|
||||
# Add 'null' and 'blank', if the 'null_ok' flag was present in the
|
||||
# table description.
|
||||
if row.null_ok: # If it's NULL...
|
||||
extra_params["blank"] = True
|
||||
extra_params["null"] = True
|
||||
|
||||
field_desc = "%s = %s%s" % (
|
||||
att_name,
|
||||
# Custom fields will have a dotted path
|
||||
"" if "." in field_type else "models.",
|
||||
field_type,
|
||||
)
|
||||
if field_type.startswith(("ForeignKey(", "OneToOneField(")):
|
||||
field_desc += ", models.DO_NOTHING"
|
||||
|
||||
# Add comment.
|
||||
if connection.features.supports_comments and row.comment:
|
||||
extra_params["db_comment"] = row.comment
|
||||
|
||||
if extra_params:
|
||||
if not field_desc.endswith("("):
|
||||
field_desc += ", "
|
||||
field_desc += ", ".join(
|
||||
"%s=%r" % (k, v) for k, v in extra_params.items()
|
||||
)
|
||||
field_desc += ")"
|
||||
if comment_notes:
|
||||
field_desc += " # " + " ".join(comment_notes)
|
||||
yield " %s" % field_desc
|
||||
comment = None
|
||||
if info := table_info.get(table_name):
|
||||
is_view = info.type == "v"
|
||||
is_partition = info.type == "p"
|
||||
if connection.features.supports_comments:
|
||||
comment = info.comment
|
||||
else:
|
||||
is_view = False
|
||||
is_partition = False
|
||||
yield from self.get_meta(
|
||||
table_name,
|
||||
constraints,
|
||||
column_to_field_name,
|
||||
is_view,
|
||||
is_partition,
|
||||
comment,
|
||||
)
|
||||
|
||||
def normalize_col_name(self, col_name, used_column_names, is_relation):
|
||||
"""
|
||||
Modify the column name to make it Python-compatible as a field name
|
||||
"""
|
||||
field_params = {}
|
||||
field_notes = []
|
||||
|
||||
new_name = col_name.lower()
|
||||
if new_name != col_name:
|
||||
field_notes.append("Field name made lowercase.")
|
||||
|
||||
if is_relation:
|
||||
if new_name.endswith("_id"):
|
||||
new_name = new_name.removesuffix("_id")
|
||||
else:
|
||||
field_params["db_column"] = col_name
|
||||
|
||||
new_name, num_repl = re.subn(r"\W", "_", new_name)
|
||||
if num_repl > 0:
|
||||
field_notes.append("Field renamed to remove unsuitable characters.")
|
||||
|
||||
if new_name.find(LOOKUP_SEP) >= 0:
|
||||
while new_name.find(LOOKUP_SEP) >= 0:
|
||||
new_name = new_name.replace(LOOKUP_SEP, "_")
|
||||
if col_name.lower().find(LOOKUP_SEP) >= 0:
|
||||
# Only add the comment if the double underscore was in the original name
|
||||
field_notes.append(
|
||||
"Field renamed because it contained more than one '_' in a row."
|
||||
)
|
||||
|
||||
if new_name.startswith("_"):
|
||||
new_name = "field%s" % new_name
|
||||
field_notes.append("Field renamed because it started with '_'.")
|
||||
|
||||
if new_name.endswith("_"):
|
||||
new_name = "%sfield" % new_name
|
||||
field_notes.append("Field renamed because it ended with '_'.")
|
||||
|
||||
if keyword.iskeyword(new_name):
|
||||
new_name += "_field"
|
||||
field_notes.append("Field renamed because it was a Python reserved word.")
|
||||
|
||||
if new_name[0].isdigit():
|
||||
new_name = "number_%s" % new_name
|
||||
field_notes.append(
|
||||
"Field renamed because it wasn't a valid Python identifier."
|
||||
)
|
||||
|
||||
if new_name in used_column_names:
|
||||
num = 0
|
||||
while "%s_%d" % (new_name, num) in used_column_names:
|
||||
num += 1
|
||||
new_name = "%s_%d" % (new_name, num)
|
||||
field_notes.append("Field renamed because of name conflict.")
|
||||
|
||||
if col_name != new_name and field_notes:
|
||||
field_params["db_column"] = col_name
|
||||
|
||||
return new_name, field_params, field_notes
|
||||
|
||||
def normalize_table_name(self, table_name):
|
||||
"""Translate the table name to a Python-compatible model name."""
|
||||
return re.sub(r"[^a-zA-Z0-9]", "", table_name.title())
|
||||
|
||||
def get_field_type(self, connection, table_name, row):
|
||||
"""
|
||||
Given the database connection, the table name, and the cursor row
|
||||
description, this routine will return the given field type name, as
|
||||
well as any additional keyword parameters and notes for the field.
|
||||
"""
|
||||
field_params = {}
|
||||
field_notes = []
|
||||
|
||||
try:
|
||||
field_type = connection.introspection.get_field_type(row.type_code, row)
|
||||
except KeyError:
|
||||
field_type = "TextField"
|
||||
field_notes.append("This field type is a guess.")
|
||||
|
||||
# Add max_length for all CharFields.
|
||||
if field_type == "CharField" and row.display_size:
|
||||
if (size := int(row.display_size)) and size > 0:
|
||||
field_params["max_length"] = size
|
||||
|
||||
if field_type in {"CharField", "TextField"} and row.collation:
|
||||
field_params["db_collation"] = row.collation
|
||||
|
||||
if field_type == "DecimalField":
|
||||
if row.precision is None or row.scale is None:
|
||||
field_notes.append(
|
||||
"max_digits and decimal_places have been guessed, as this "
|
||||
"database handles decimal fields as float"
|
||||
)
|
||||
field_params["max_digits"] = (
|
||||
row.precision if row.precision is not None else 10
|
||||
)
|
||||
field_params["decimal_places"] = (
|
||||
row.scale if row.scale is not None else 5
|
||||
)
|
||||
else:
|
||||
field_params["max_digits"] = row.precision
|
||||
field_params["decimal_places"] = row.scale
|
||||
|
||||
return field_type, field_params, field_notes
|
||||
|
||||
def get_meta(
|
||||
self,
|
||||
table_name,
|
||||
constraints,
|
||||
column_to_field_name,
|
||||
is_view,
|
||||
is_partition,
|
||||
comment,
|
||||
):
|
||||
"""
|
||||
Return a sequence comprising the lines of code necessary
|
||||
to construct the inner Meta class for the model corresponding
|
||||
to the given database table name.
|
||||
"""
|
||||
unique_together = []
|
||||
has_unsupported_constraint = False
|
||||
for params in constraints.values():
|
||||
if params["unique"]:
|
||||
columns = params["columns"]
|
||||
if None in columns:
|
||||
has_unsupported_constraint = True
|
||||
columns = [
|
||||
x for x in columns if x is not None and x in column_to_field_name
|
||||
]
|
||||
if len(columns) > 1 and not params["primary_key"]:
|
||||
unique_together.append(
|
||||
str(tuple(column_to_field_name[c] for c in columns))
|
||||
)
|
||||
if is_view:
|
||||
managed_comment = " # Created from a view. Don't remove."
|
||||
elif is_partition:
|
||||
managed_comment = " # Created from a partition. Don't remove."
|
||||
else:
|
||||
managed_comment = ""
|
||||
meta = [""]
|
||||
if has_unsupported_constraint:
|
||||
meta.append(" # A unique constraint could not be introspected.")
|
||||
meta += [
|
||||
" class Meta:",
|
||||
" managed = False%s" % managed_comment,
|
||||
" db_table = %r" % table_name,
|
||||
]
|
||||
if unique_together:
|
||||
tup = "(" + ", ".join(unique_together) + ",)"
|
||||
meta += [" unique_together = %s" % tup]
|
||||
if comment:
|
||||
meta += [f" db_table_comment = {comment!r}"]
|
||||
return meta
|
||||
@@ -0,0 +1,433 @@
|
||||
import functools
|
||||
import glob
|
||||
import gzip
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
import zipfile
|
||||
from itertools import product
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core import serializers
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.color import no_style
|
||||
from django.core.management.utils import parse_apps_and_model_labels
|
||||
from django.db import (
|
||||
DEFAULT_DB_ALIAS,
|
||||
DatabaseError,
|
||||
IntegrityError,
|
||||
connections,
|
||||
router,
|
||||
transaction,
|
||||
)
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
try:
|
||||
import bz2
|
||||
|
||||
has_bz2 = True
|
||||
except ImportError:
|
||||
has_bz2 = False
|
||||
|
||||
try:
|
||||
import lzma
|
||||
|
||||
has_lzma = True
|
||||
except ImportError:
|
||||
has_lzma = False
|
||||
|
||||
READ_STDIN = "-"
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Installs the named fixture(s) in the database."
|
||||
missing_args_message = (
|
||||
"No database fixture specified. Please provide the path of at least "
|
||||
"one fixture in the command line."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"args", metavar="fixture", nargs="+", help="Fixture labels."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help=(
|
||||
"Nominates a specific database to load fixtures into. Defaults to the "
|
||||
'"default" database.'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--app",
|
||||
dest="app_label",
|
||||
help="Only look for fixtures in the specified app.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignorenonexistent",
|
||||
"-i",
|
||||
action="store_true",
|
||||
dest="ignore",
|
||||
help="Ignores entries in the serialized data for fields that do not "
|
||||
"currently exist on the model.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--exclude",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"An app_label or app_label.ModelName to exclude. Can be used multiple "
|
||||
"times."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
help="Format of serialized data when reading from stdin.",
|
||||
)
|
||||
|
||||
def handle(self, *fixture_labels, **options):
|
||||
self.ignore = options["ignore"]
|
||||
self.using = options["database"]
|
||||
self.app_label = options["app_label"]
|
||||
self.verbosity = options["verbosity"]
|
||||
self.excluded_models, self.excluded_apps = parse_apps_and_model_labels(
|
||||
options["exclude"]
|
||||
)
|
||||
self.format = options["format"]
|
||||
|
||||
with transaction.atomic(using=self.using):
|
||||
self.loaddata(fixture_labels)
|
||||
|
||||
# Close the DB connection -- unless we're still in a transaction. This
|
||||
# is required as a workaround for an edge case in MySQL: if the same
|
||||
# connection is used to create tables, load data, and query, the query
|
||||
# can return incorrect results. See Django #7572, MySQL #37735.
|
||||
if transaction.get_autocommit(self.using):
|
||||
connections[self.using].close()
|
||||
|
||||
@cached_property
|
||||
def compression_formats(self):
|
||||
"""A dict mapping format names to (open function, mode arg) tuples."""
|
||||
# Forcing binary mode may be revisited after dropping Python 2 support
|
||||
# (see #22399).
|
||||
compression_formats = {
|
||||
None: (open, "rb"),
|
||||
"gz": (gzip.GzipFile, "rb"),
|
||||
"zip": (SingleZipReader, "r"),
|
||||
"stdin": (lambda *args: sys.stdin, None),
|
||||
}
|
||||
if has_bz2:
|
||||
compression_formats["bz2"] = (bz2.BZ2File, "r")
|
||||
if has_lzma:
|
||||
compression_formats["lzma"] = (lzma.LZMAFile, "r")
|
||||
compression_formats["xz"] = (lzma.LZMAFile, "r")
|
||||
return compression_formats
|
||||
|
||||
def reset_sequences(self, connection, models):
|
||||
"""Reset database sequences for the given connection and models."""
|
||||
sequence_sql = connection.ops.sequence_reset_sql(no_style(), models)
|
||||
if sequence_sql:
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Resetting sequences")
|
||||
with connection.cursor() as cursor:
|
||||
for line in sequence_sql:
|
||||
cursor.execute(line)
|
||||
|
||||
def loaddata(self, fixture_labels):
|
||||
connection = connections[self.using]
|
||||
|
||||
# Keep a count of the installed objects and fixtures
|
||||
self.fixture_count = 0
|
||||
self.loaded_object_count = 0
|
||||
self.fixture_object_count = 0
|
||||
self.models = set()
|
||||
|
||||
self.serialization_formats = serializers.get_public_serializer_formats()
|
||||
|
||||
# Django's test suite repeatedly tries to load initial_data fixtures
|
||||
# from apps that don't have any fixtures. Because disabling constraint
|
||||
# checks can be expensive on some database (especially MSSQL), bail
|
||||
# out early if no fixtures are found.
|
||||
for fixture_label in fixture_labels:
|
||||
if self.find_fixtures(fixture_label):
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
self.objs_with_deferred_fields = []
|
||||
with connection.constraint_checks_disabled():
|
||||
for fixture_label in fixture_labels:
|
||||
self.load_label(fixture_label)
|
||||
for obj in self.objs_with_deferred_fields:
|
||||
obj.save_deferred_fields(using=self.using)
|
||||
|
||||
# Since we disabled constraint checks, we must manually check for
|
||||
# any invalid keys that might have been added
|
||||
table_names = [model._meta.db_table for model in self.models]
|
||||
try:
|
||||
connection.check_constraints(table_names=table_names)
|
||||
except Exception as e:
|
||||
e.args = ("Problem installing fixtures: %s" % e,)
|
||||
raise
|
||||
|
||||
# If we found even one object in a fixture, we need to reset the
|
||||
# database sequences.
|
||||
if self.loaded_object_count > 0:
|
||||
self.reset_sequences(connection, self.models)
|
||||
|
||||
if self.verbosity >= 1:
|
||||
if self.fixture_object_count == self.loaded_object_count:
|
||||
self.stdout.write(
|
||||
"Installed %d object(s) from %d fixture(s)"
|
||||
% (self.loaded_object_count, self.fixture_count)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
"Installed %d object(s) (of %d) from %d fixture(s)"
|
||||
% (
|
||||
self.loaded_object_count,
|
||||
self.fixture_object_count,
|
||||
self.fixture_count,
|
||||
)
|
||||
)
|
||||
|
||||
def save_obj(self, obj):
|
||||
"""Save an object if permitted."""
|
||||
if (
|
||||
obj.object._meta.app_config in self.excluded_apps
|
||||
or type(obj.object) in self.excluded_models
|
||||
):
|
||||
return False
|
||||
saved = False
|
||||
if router.allow_migrate_model(self.using, obj.object.__class__):
|
||||
saved = True
|
||||
self.models.add(obj.object.__class__)
|
||||
try:
|
||||
obj.save(using=self.using)
|
||||
# psycopg raises ValueError if data contains NUL chars.
|
||||
except (DatabaseError, IntegrityError, ValueError) as e:
|
||||
e.args = (
|
||||
"Could not load %(object_label)s(pk=%(pk)s): %(error_msg)s"
|
||||
% {
|
||||
"object_label": obj.object._meta.label,
|
||||
"pk": obj.object.pk,
|
||||
"error_msg": e,
|
||||
},
|
||||
)
|
||||
raise
|
||||
if obj.deferred_fields:
|
||||
self.objs_with_deferred_fields.append(obj)
|
||||
return saved
|
||||
|
||||
def load_label(self, fixture_label):
|
||||
"""Load fixtures files for a given label."""
|
||||
show_progress = self.verbosity >= 3
|
||||
for fixture_file, fixture_dir, fixture_name in self.find_fixtures(
|
||||
fixture_label
|
||||
):
|
||||
_, ser_fmt, cmp_fmt = self.parse_name(os.path.basename(fixture_file))
|
||||
open_method, mode = self.compression_formats[cmp_fmt]
|
||||
fixture = open_method(fixture_file, mode)
|
||||
self.fixture_count += 1
|
||||
objects_in_fixture = 0
|
||||
loaded_objects_in_fixture = 0
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write(
|
||||
"Installing %s fixture '%s' from %s."
|
||||
% (ser_fmt, fixture_name, humanize(fixture_dir))
|
||||
)
|
||||
try:
|
||||
objects = serializers.deserialize(
|
||||
ser_fmt,
|
||||
fixture,
|
||||
using=self.using,
|
||||
ignorenonexistent=self.ignore,
|
||||
handle_forward_references=True,
|
||||
)
|
||||
|
||||
for obj in objects:
|
||||
objects_in_fixture += 1
|
||||
if self.save_obj(obj):
|
||||
loaded_objects_in_fixture += 1
|
||||
if show_progress:
|
||||
self.stdout.write(
|
||||
"\rProcessed %i object(s)." % loaded_objects_in_fixture,
|
||||
ending="",
|
||||
)
|
||||
except Exception as e:
|
||||
if not isinstance(e, CommandError):
|
||||
e.args = (
|
||||
"Problem installing fixture '%s': %s" % (fixture_file, e),
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
fixture.close()
|
||||
|
||||
if objects_in_fixture and show_progress:
|
||||
self.stdout.write() # Add a newline after progress indicator.
|
||||
self.loaded_object_count += loaded_objects_in_fixture
|
||||
self.fixture_object_count += objects_in_fixture
|
||||
# Warn if the fixture we loaded contains 0 objects.
|
||||
if objects_in_fixture == 0:
|
||||
warnings.warn(
|
||||
"No fixture data found for '%s'. (File format may be "
|
||||
"invalid.)" % fixture_name,
|
||||
RuntimeWarning,
|
||||
)
|
||||
|
||||
def get_fixture_name_and_dirs(self, fixture_name):
|
||||
dirname, basename = os.path.split(fixture_name)
|
||||
if os.path.isabs(fixture_name):
|
||||
fixture_dirs = [dirname]
|
||||
else:
|
||||
fixture_dirs = self.fixture_dirs
|
||||
if os.path.sep in os.path.normpath(fixture_name):
|
||||
fixture_dirs = [os.path.join(dir_, dirname) for dir_ in fixture_dirs]
|
||||
return basename, fixture_dirs
|
||||
|
||||
def get_targets(self, fixture_name, ser_fmt, cmp_fmt):
|
||||
databases = [self.using, None]
|
||||
cmp_fmts = self.compression_formats if cmp_fmt is None else [cmp_fmt]
|
||||
ser_fmts = self.serialization_formats if ser_fmt is None else [ser_fmt]
|
||||
return {
|
||||
"%s.%s"
|
||||
% (
|
||||
fixture_name,
|
||||
".".join([ext for ext in combo if ext]),
|
||||
)
|
||||
for combo in product(databases, ser_fmts, cmp_fmts)
|
||||
}
|
||||
|
||||
def find_fixture_files_in_dir(self, fixture_dir, fixture_name, targets):
|
||||
fixture_files_in_dir = []
|
||||
path = os.path.join(fixture_dir, fixture_name)
|
||||
for candidate in glob.iglob(glob.escape(path) + "*"):
|
||||
if os.path.basename(candidate) in targets:
|
||||
# Save the fixture_dir and fixture_name for future error
|
||||
# messages.
|
||||
fixture_files_in_dir.append((candidate, fixture_dir, fixture_name))
|
||||
return fixture_files_in_dir
|
||||
|
||||
@functools.cache
|
||||
def find_fixtures(self, fixture_label):
|
||||
"""Find fixture files for a given label."""
|
||||
if fixture_label == READ_STDIN:
|
||||
return [(READ_STDIN, None, READ_STDIN)]
|
||||
|
||||
fixture_name, ser_fmt, cmp_fmt = self.parse_name(fixture_label)
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Loading '%s' fixtures..." % fixture_name)
|
||||
|
||||
fixture_name, fixture_dirs = self.get_fixture_name_and_dirs(fixture_name)
|
||||
targets = self.get_targets(fixture_name, ser_fmt, cmp_fmt)
|
||||
fixture_files = []
|
||||
for fixture_dir in fixture_dirs:
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Checking %s for fixtures..." % humanize(fixture_dir))
|
||||
fixture_files_in_dir = self.find_fixture_files_in_dir(
|
||||
fixture_dir,
|
||||
fixture_name,
|
||||
targets,
|
||||
)
|
||||
if self.verbosity >= 2 and not fixture_files_in_dir:
|
||||
self.stdout.write(
|
||||
"No fixture '%s' in %s." % (fixture_name, humanize(fixture_dir))
|
||||
)
|
||||
|
||||
# Check kept for backwards-compatibility; it isn't clear why
|
||||
# duplicates are only allowed in different directories.
|
||||
if len(fixture_files_in_dir) > 1:
|
||||
raise CommandError(
|
||||
"Multiple fixtures named '%s' in %s. Aborting."
|
||||
% (fixture_name, humanize(fixture_dir))
|
||||
)
|
||||
fixture_files.extend(fixture_files_in_dir)
|
||||
|
||||
if not fixture_files:
|
||||
raise CommandError("No fixture named '%s' found." % fixture_name)
|
||||
|
||||
return fixture_files
|
||||
|
||||
@cached_property
|
||||
def fixture_dirs(self):
|
||||
"""
|
||||
Return a list of fixture directories.
|
||||
|
||||
The list contains the 'fixtures' subdirectory of each installed
|
||||
application, if it exists, the directories in FIXTURE_DIRS, and the
|
||||
current directory.
|
||||
"""
|
||||
dirs = []
|
||||
fixture_dirs = settings.FIXTURE_DIRS
|
||||
if len(fixture_dirs) != len(set(fixture_dirs)):
|
||||
raise ImproperlyConfigured("settings.FIXTURE_DIRS contains duplicates.")
|
||||
for app_config in apps.get_app_configs():
|
||||
app_label = app_config.label
|
||||
app_dir = os.path.join(app_config.path, "fixtures")
|
||||
if app_dir in [str(d) for d in fixture_dirs]:
|
||||
raise ImproperlyConfigured(
|
||||
"'%s' is a default fixture directory for the '%s' app "
|
||||
"and cannot be listed in settings.FIXTURE_DIRS."
|
||||
% (app_dir, app_label)
|
||||
)
|
||||
|
||||
if self.app_label and app_label != self.app_label:
|
||||
continue
|
||||
if os.path.isdir(app_dir):
|
||||
dirs.append(app_dir)
|
||||
dirs.extend(fixture_dirs)
|
||||
dirs.append("")
|
||||
return [os.path.realpath(d) for d in dirs]
|
||||
|
||||
def parse_name(self, fixture_name):
|
||||
"""
|
||||
Split fixture name in name, serialization format, compression format.
|
||||
"""
|
||||
if fixture_name == READ_STDIN:
|
||||
if not self.format:
|
||||
raise CommandError(
|
||||
"--format must be specified when reading from stdin."
|
||||
)
|
||||
return READ_STDIN, self.format, "stdin"
|
||||
|
||||
parts = fixture_name.rsplit(".", 2)
|
||||
|
||||
if len(parts) > 1 and parts[-1] in self.compression_formats:
|
||||
cmp_fmt = parts[-1]
|
||||
parts = parts[:-1]
|
||||
else:
|
||||
cmp_fmt = None
|
||||
|
||||
if len(parts) > 1:
|
||||
if parts[-1] in self.serialization_formats:
|
||||
ser_fmt = parts[-1]
|
||||
parts = parts[:-1]
|
||||
else:
|
||||
raise CommandError(
|
||||
"Problem installing fixture '%s': %s is not a known "
|
||||
"serialization format." % (".".join(parts[:-1]), parts[-1])
|
||||
)
|
||||
else:
|
||||
ser_fmt = None
|
||||
|
||||
name = ".".join(parts)
|
||||
|
||||
return name, ser_fmt, cmp_fmt
|
||||
|
||||
|
||||
class SingleZipReader(zipfile.ZipFile):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if len(self.namelist()) != 1:
|
||||
raise ValueError("Zip-compressed fixtures must contain one file.")
|
||||
|
||||
def read(self):
|
||||
return zipfile.ZipFile.read(self, self.namelist()[0])
|
||||
|
||||
|
||||
def humanize(dirname):
|
||||
return "'%s'" % dirname if dirname else "absolute path"
|
||||
@@ -0,0 +1,769 @@
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from functools import total_ordering
|
||||
from itertools import dropwhile
|
||||
from pathlib import Path
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files.temp import NamedTemporaryFile
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.utils import (
|
||||
find_command,
|
||||
handle_extensions,
|
||||
is_ignored_path,
|
||||
popen_wrapper,
|
||||
)
|
||||
from django.utils.encoding import DEFAULT_LOCALE_ENCODING
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.text import get_text_list
|
||||
from django.utils.translation import templatize
|
||||
|
||||
plural_forms_re = _lazy_re_compile(
|
||||
r'^(?P<value>"Plural-Forms.+?\\n")\s*$', re.MULTILINE | re.DOTALL
|
||||
)
|
||||
STATUS_OK = 0
|
||||
NO_LOCALE_DIR = object()
|
||||
|
||||
|
||||
def check_programs(*programs):
|
||||
for program in programs:
|
||||
if find_command(program) is None:
|
||||
raise CommandError(
|
||||
f"Can't find {program}. Make sure you have GNU gettext tools "
|
||||
"0.19 or newer installed."
|
||||
)
|
||||
|
||||
|
||||
def is_valid_locale(locale):
|
||||
return re.match(r"^[a-z]+$", locale) or re.match(r"^[a-z]+_[A-Z0-9].*$", locale)
|
||||
|
||||
|
||||
@total_ordering
|
||||
class TranslatableFile:
|
||||
def __init__(self, dirpath, file_name, locale_dir):
|
||||
self.file = file_name
|
||||
self.dirpath = dirpath
|
||||
self.locale_dir = locale_dir
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: %s>" % (
|
||||
self.__class__.__name__,
|
||||
os.sep.join([self.dirpath, self.file]),
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.path == other.path
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.path < other.path
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return os.path.join(self.dirpath, self.file)
|
||||
|
||||
|
||||
class BuildFile:
|
||||
"""
|
||||
Represent the state of a translatable file during the build process.
|
||||
"""
|
||||
|
||||
def __init__(self, command, domain, translatable):
|
||||
self.command = command
|
||||
self.domain = domain
|
||||
self.translatable = translatable
|
||||
|
||||
@cached_property
|
||||
def is_templatized(self):
|
||||
if self.domain == "django":
|
||||
file_ext = os.path.splitext(self.translatable.file)[1]
|
||||
return file_ext != ".py"
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def path(self):
|
||||
return self.translatable.path
|
||||
|
||||
@cached_property
|
||||
def work_path(self):
|
||||
"""
|
||||
Path to a file which is being fed into GNU gettext pipeline. This may
|
||||
be either a translatable or its preprocessed version.
|
||||
"""
|
||||
if not self.is_templatized:
|
||||
return self.path
|
||||
filename = f"{self.translatable.file}.py"
|
||||
return os.path.join(self.translatable.dirpath, filename)
|
||||
|
||||
def preprocess(self):
|
||||
"""
|
||||
Preprocess (if necessary) a translatable file before passing it to
|
||||
xgettext GNU gettext utility.
|
||||
"""
|
||||
if not self.is_templatized:
|
||||
return
|
||||
|
||||
with open(self.path, encoding="utf-8") as fp:
|
||||
src_data = fp.read()
|
||||
|
||||
if self.domain == "django":
|
||||
content = templatize(src_data, origin=self.path[2:])
|
||||
|
||||
with open(self.work_path, "w", encoding="utf-8") as fp:
|
||||
fp.write(content)
|
||||
|
||||
def postprocess_messages(self, msgs):
|
||||
"""
|
||||
Postprocess messages generated by xgettext GNU gettext utility.
|
||||
|
||||
Transform paths as if these messages were generated from original
|
||||
translatable files rather than from preprocessed versions.
|
||||
"""
|
||||
if not self.is_templatized:
|
||||
return msgs
|
||||
|
||||
# Remove '.py' suffix
|
||||
if os.name == "nt":
|
||||
# Preserve '.\' prefix on Windows to respect gettext behavior
|
||||
old_path = self.work_path
|
||||
new_path = self.path
|
||||
else:
|
||||
old_path = self.work_path[2:]
|
||||
new_path = self.path[2:]
|
||||
|
||||
return re.sub(
|
||||
r"^(#: .*)(" + re.escape(old_path) + r")",
|
||||
lambda match: match[0].replace(old_path, new_path),
|
||||
msgs,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Remove a preprocessed copy of a translatable file (if any).
|
||||
"""
|
||||
if self.is_templatized:
|
||||
# This check is needed for the case of a symlinked file and its
|
||||
# source being processed inside a single group (locale dir);
|
||||
# removing either of those two removes both.
|
||||
if os.path.exists(self.work_path):
|
||||
os.unlink(self.work_path)
|
||||
|
||||
|
||||
def normalize_eols(raw_contents):
|
||||
"""
|
||||
Take a block of raw text that will be passed through str.splitlines() to
|
||||
get universal newlines treatment.
|
||||
|
||||
Return the resulting block of text with normalized `\n` EOL sequences ready
|
||||
to be written to disk using current platform's native EOLs.
|
||||
"""
|
||||
lines_list = raw_contents.splitlines()
|
||||
# Ensure last line has its EOL
|
||||
if lines_list and lines_list[-1]:
|
||||
lines_list.append("")
|
||||
return "\n".join(lines_list)
|
||||
|
||||
|
||||
def write_pot_file(potfile, msgs):
|
||||
"""
|
||||
Write the `potfile` with the `msgs` contents, making sure its format is
|
||||
valid.
|
||||
"""
|
||||
pot_lines = msgs.splitlines()
|
||||
if os.path.exists(potfile):
|
||||
# Strip the header
|
||||
lines = dropwhile(len, pot_lines)
|
||||
else:
|
||||
lines = []
|
||||
found, header_read = False, False
|
||||
for line in pot_lines:
|
||||
if not found and not header_read:
|
||||
if "charset=CHARSET" in line:
|
||||
found = True
|
||||
line = line.replace("charset=CHARSET", "charset=UTF-8")
|
||||
if not line and not found:
|
||||
header_read = True
|
||||
lines.append(line)
|
||||
msgs = "\n".join(lines)
|
||||
# Force newlines of POT files to '\n' to work around
|
||||
# https://savannah.gnu.org/bugs/index.php?52395
|
||||
with open(potfile, "a", encoding="utf-8", newline="\n") as fp:
|
||||
fp.write(msgs)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Runs over the entire source tree of the current directory and pulls out all "
|
||||
"strings marked for translation. It creates (or updates) a message file in the "
|
||||
"conf/locale (in the django tree) or locale (for projects and applications) "
|
||||
"directory.\n\nYou must run this command with one of either the --locale, "
|
||||
"--exclude, or --all options."
|
||||
)
|
||||
|
||||
translatable_file_class = TranslatableFile
|
||||
build_file_class = BuildFile
|
||||
|
||||
requires_system_checks = []
|
||||
|
||||
msgmerge_options = ["-q", "--backup=none", "--previous", "--update"]
|
||||
msguniq_options = ["--to-code=utf-8"]
|
||||
msgattrib_options = ["--no-obsolete"]
|
||||
xgettext_options = ["--from-code=UTF-8", "--add-comments=Translators"]
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--locale",
|
||||
"-l",
|
||||
default=[],
|
||||
action="append",
|
||||
help=(
|
||||
"Creates or updates the message files for the given locale(s) (e.g. "
|
||||
"pt_BR). Can be used multiple times."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exclude",
|
||||
"-x",
|
||||
default=[],
|
||||
action="append",
|
||||
help="Locales to exclude. Default is none. Can be used multiple times.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--domain",
|
||||
"-d",
|
||||
default="django",
|
||||
help='The domain of the message files (default: "django").',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all",
|
||||
"-a",
|
||||
action="store_true",
|
||||
help="Updates the message files for all existing locales.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extension",
|
||||
"-e",
|
||||
dest="extensions",
|
||||
action="append",
|
||||
help='The file extension(s) to examine (default: "html,txt,py", or "js" '
|
||||
'if the domain is "djangojs"). Separate multiple extensions with '
|
||||
"commas, or use -e multiple times.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--symlinks",
|
||||
"-s",
|
||||
action="store_true",
|
||||
help="Follows symlinks to directories when examining source code "
|
||||
"and templates for translation strings.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore",
|
||||
"-i",
|
||||
action="append",
|
||||
dest="ignore_patterns",
|
||||
default=[],
|
||||
metavar="PATTERN",
|
||||
help="Ignore files or directories matching this glob-style pattern. "
|
||||
"Use multiple times to ignore more.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-default-ignore",
|
||||
action="store_false",
|
||||
dest="use_default_ignore_patterns",
|
||||
help=(
|
||||
"Don't ignore the common glob-style patterns 'CVS', '.*', '*~' and "
|
||||
"'*.pyc'."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-wrap",
|
||||
action="store_true",
|
||||
help="Don't break long message lines into several lines.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-location",
|
||||
action="store_true",
|
||||
help="Don't write '#: filename:line' lines.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--add-location",
|
||||
choices=("full", "file", "never"),
|
||||
const="full",
|
||||
nargs="?",
|
||||
help=(
|
||||
"Controls '#: filename:line' lines. If the option is 'full' "
|
||||
"(the default if not given), the lines include both file name "
|
||||
"and line number. If it's 'file', the line number is omitted. If "
|
||||
"it's 'never', the lines are suppressed (same as --no-location). "
|
||||
"--add-location requires gettext 0.19 or newer."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-obsolete",
|
||||
action="store_true",
|
||||
help="Remove obsolete message strings.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep-pot",
|
||||
action="store_true",
|
||||
help="Keep .pot file after making messages. Useful when debugging.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
locale = options["locale"]
|
||||
exclude = options["exclude"]
|
||||
self.domain = options["domain"]
|
||||
self.verbosity = options["verbosity"]
|
||||
process_all = options["all"]
|
||||
extensions = options["extensions"]
|
||||
self.symlinks = options["symlinks"]
|
||||
|
||||
ignore_patterns = options["ignore_patterns"]
|
||||
if options["use_default_ignore_patterns"]:
|
||||
ignore_patterns += ["CVS", ".*", "*~", "*.pyc"]
|
||||
self.ignore_patterns = list(set(ignore_patterns))
|
||||
|
||||
# Avoid messing with mutable class variables
|
||||
if options["no_wrap"]:
|
||||
self.msgmerge_options = self.msgmerge_options[:] + ["--no-wrap"]
|
||||
self.msguniq_options = self.msguniq_options[:] + ["--no-wrap"]
|
||||
self.msgattrib_options = self.msgattrib_options[:] + ["--no-wrap"]
|
||||
self.xgettext_options = self.xgettext_options[:] + ["--no-wrap"]
|
||||
if options["no_location"]:
|
||||
self.msgmerge_options = self.msgmerge_options[:] + ["--no-location"]
|
||||
self.msguniq_options = self.msguniq_options[:] + ["--no-location"]
|
||||
self.msgattrib_options = self.msgattrib_options[:] + ["--no-location"]
|
||||
self.xgettext_options = self.xgettext_options[:] + ["--no-location"]
|
||||
if options["add_location"]:
|
||||
arg_add_location = "--add-location=%s" % options["add_location"]
|
||||
self.msgmerge_options = self.msgmerge_options[:] + [arg_add_location]
|
||||
self.msguniq_options = self.msguniq_options[:] + [arg_add_location]
|
||||
self.msgattrib_options = self.msgattrib_options[:] + [arg_add_location]
|
||||
self.xgettext_options = self.xgettext_options[:] + [arg_add_location]
|
||||
|
||||
self.no_obsolete = options["no_obsolete"]
|
||||
self.keep_pot = options["keep_pot"]
|
||||
|
||||
if self.domain not in ("django", "djangojs"):
|
||||
raise CommandError(
|
||||
"currently makemessages only supports domains "
|
||||
"'django' and 'djangojs'"
|
||||
)
|
||||
if self.domain == "djangojs":
|
||||
exts = extensions or ["js"]
|
||||
else:
|
||||
exts = extensions or ["html", "txt", "py"]
|
||||
self.extensions = handle_extensions(exts)
|
||||
|
||||
if (not locale and not exclude and not process_all) or self.domain is None:
|
||||
raise CommandError(
|
||||
"Type '%s help %s' for usage information."
|
||||
% (os.path.basename(sys.argv[0]), sys.argv[1])
|
||||
)
|
||||
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write(
|
||||
"examining files with the extensions: %s"
|
||||
% get_text_list(list(self.extensions), "and")
|
||||
)
|
||||
|
||||
self.invoked_for_django = False
|
||||
self.locale_paths = []
|
||||
self.default_locale_path = None
|
||||
if os.path.isdir(os.path.join("conf", "locale")):
|
||||
self.locale_paths = [os.path.abspath(os.path.join("conf", "locale"))]
|
||||
self.default_locale_path = self.locale_paths[0]
|
||||
self.ignore_patterns.append("views/templates/i18n_catalog.js")
|
||||
self.invoked_for_django = True
|
||||
else:
|
||||
if self.settings_available:
|
||||
self.locale_paths.extend(settings.LOCALE_PATHS)
|
||||
# Allow to run makemessages inside an app dir
|
||||
if os.path.isdir("locale"):
|
||||
self.locale_paths.append(os.path.abspath("locale"))
|
||||
if self.locale_paths:
|
||||
self.default_locale_path = self.locale_paths[0]
|
||||
os.makedirs(self.default_locale_path, exist_ok=True)
|
||||
|
||||
# Build locale list
|
||||
looks_like_locale = re.compile(r"[a-z]{2}")
|
||||
locale_dirs = filter(
|
||||
os.path.isdir, glob.glob("%s/*" % self.default_locale_path)
|
||||
)
|
||||
all_locales = [
|
||||
lang_code
|
||||
for lang_code in map(os.path.basename, locale_dirs)
|
||||
if looks_like_locale.match(lang_code)
|
||||
]
|
||||
|
||||
# Account for excluded locales
|
||||
if process_all:
|
||||
locales = all_locales
|
||||
else:
|
||||
locales = locale or all_locales
|
||||
locales = set(locales).difference(exclude)
|
||||
|
||||
if locales:
|
||||
check_programs("msguniq", "msgmerge", "msgattrib")
|
||||
|
||||
check_programs("xgettext")
|
||||
|
||||
try:
|
||||
potfiles = self.build_potfiles()
|
||||
|
||||
# Build po files for each selected locale
|
||||
for locale in locales:
|
||||
if not is_valid_locale(locale):
|
||||
# Try to guess what valid locale it could be
|
||||
# Valid examples are: en_GB, shi_Latn_MA and nl_NL-x-informal
|
||||
|
||||
# Search for characters followed by a non character (i.e. separator)
|
||||
match = re.match(
|
||||
r"^(?P<language>[a-zA-Z]+)"
|
||||
r"(?P<separator>[^a-zA-Z])"
|
||||
r"(?P<territory>.+)$",
|
||||
locale,
|
||||
)
|
||||
if match:
|
||||
locale_parts = match.groupdict()
|
||||
language = locale_parts["language"].lower()
|
||||
territory = (
|
||||
locale_parts["territory"][:2].upper()
|
||||
+ locale_parts["territory"][2:]
|
||||
)
|
||||
proposed_locale = f"{language}_{territory}"
|
||||
else:
|
||||
# It could be a language in uppercase
|
||||
proposed_locale = locale.lower()
|
||||
|
||||
# Recheck if the proposed locale is valid
|
||||
if is_valid_locale(proposed_locale):
|
||||
self.stdout.write(
|
||||
"invalid locale %s, did you mean %s?"
|
||||
% (
|
||||
locale,
|
||||
proposed_locale,
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.stdout.write("invalid locale %s" % locale)
|
||||
|
||||
continue
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write("processing locale %s" % locale)
|
||||
for potfile in potfiles:
|
||||
self.write_po_file(potfile, locale)
|
||||
finally:
|
||||
if not self.keep_pot:
|
||||
self.remove_potfiles()
|
||||
|
||||
@cached_property
|
||||
def gettext_version(self):
|
||||
# Gettext tools will output system-encoded bytestrings instead of UTF-8,
|
||||
# when looking up the version. It's especially a problem on Windows.
|
||||
out, err, status = popen_wrapper(
|
||||
["xgettext", "--version"],
|
||||
stdout_encoding=DEFAULT_LOCALE_ENCODING,
|
||||
)
|
||||
m = re.search(r"(\d+)\.(\d+)\.?(\d+)?", out)
|
||||
if m:
|
||||
return tuple(int(d) for d in m.groups() if d is not None)
|
||||
else:
|
||||
raise CommandError("Unable to get gettext version. Is it installed?")
|
||||
|
||||
@cached_property
|
||||
def settings_available(self):
|
||||
try:
|
||||
settings.LOCALE_PATHS
|
||||
except ImproperlyConfigured:
|
||||
if self.verbosity > 1:
|
||||
self.stderr.write("Running without configured settings.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def build_potfiles(self):
|
||||
"""
|
||||
Build pot files and apply msguniq to them.
|
||||
"""
|
||||
file_list = self.find_files(".")
|
||||
self.remove_potfiles()
|
||||
self.process_files(file_list)
|
||||
potfiles = []
|
||||
for path in self.locale_paths:
|
||||
potfile = os.path.join(path, "%s.pot" % self.domain)
|
||||
if not os.path.exists(potfile):
|
||||
continue
|
||||
args = ["msguniq"] + self.msguniq_options + [potfile]
|
||||
msgs, errors, status = popen_wrapper(args)
|
||||
if errors:
|
||||
if status != STATUS_OK:
|
||||
raise CommandError(
|
||||
"errors happened while running msguniq\n%s" % errors
|
||||
)
|
||||
elif self.verbosity > 0:
|
||||
self.stdout.write(errors)
|
||||
msgs = normalize_eols(msgs)
|
||||
with open(potfile, "w", encoding="utf-8") as fp:
|
||||
fp.write(msgs)
|
||||
potfiles.append(potfile)
|
||||
return potfiles
|
||||
|
||||
def remove_potfiles(self):
|
||||
for path in self.locale_paths:
|
||||
pot_path = os.path.join(path, "%s.pot" % self.domain)
|
||||
if os.path.exists(pot_path):
|
||||
os.unlink(pot_path)
|
||||
|
||||
def find_files(self, root):
|
||||
"""
|
||||
Get all files in the given root. Also check that there is a matching
|
||||
locale dir for each file.
|
||||
"""
|
||||
all_files = []
|
||||
ignored_roots = []
|
||||
if self.settings_available:
|
||||
ignored_roots = [
|
||||
os.path.normpath(p)
|
||||
for p in (settings.MEDIA_ROOT, settings.STATIC_ROOT)
|
||||
if p
|
||||
]
|
||||
for dirpath, dirnames, filenames in os.walk(
|
||||
root, topdown=True, followlinks=self.symlinks
|
||||
):
|
||||
for dirname in dirnames[:]:
|
||||
if (
|
||||
is_ignored_path(
|
||||
os.path.normpath(os.path.join(dirpath, dirname)),
|
||||
self.ignore_patterns,
|
||||
)
|
||||
or os.path.join(os.path.abspath(dirpath), dirname) in ignored_roots
|
||||
):
|
||||
dirnames.remove(dirname)
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write("ignoring directory %s" % dirname)
|
||||
elif dirname == "locale":
|
||||
dirnames.remove(dirname)
|
||||
self.locale_paths.insert(
|
||||
0, os.path.join(os.path.abspath(dirpath), dirname)
|
||||
)
|
||||
for filename in filenames:
|
||||
file_path = os.path.normpath(os.path.join(dirpath, filename))
|
||||
file_ext = os.path.splitext(filename)[1]
|
||||
if file_ext not in self.extensions or is_ignored_path(
|
||||
file_path, self.ignore_patterns
|
||||
):
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write(
|
||||
"ignoring file %s in %s" % (filename, dirpath)
|
||||
)
|
||||
else:
|
||||
locale_dir = None
|
||||
for path in self.locale_paths:
|
||||
if os.path.abspath(dirpath).startswith(os.path.dirname(path)):
|
||||
locale_dir = path
|
||||
break
|
||||
locale_dir = locale_dir or self.default_locale_path or NO_LOCALE_DIR
|
||||
all_files.append(
|
||||
self.translatable_file_class(dirpath, filename, locale_dir)
|
||||
)
|
||||
return sorted(all_files)
|
||||
|
||||
def process_files(self, file_list):
|
||||
"""
|
||||
Group translatable files by locale directory and run pot file build
|
||||
process for each group.
|
||||
"""
|
||||
file_groups = {}
|
||||
for translatable in file_list:
|
||||
file_group = file_groups.setdefault(translatable.locale_dir, [])
|
||||
file_group.append(translatable)
|
||||
for locale_dir, files in file_groups.items():
|
||||
self.process_locale_dir(locale_dir, files)
|
||||
|
||||
def process_locale_dir(self, locale_dir, files):
|
||||
"""
|
||||
Extract translatable literals from the specified files, creating or
|
||||
updating the POT file for a given locale directory.
|
||||
|
||||
Use the xgettext GNU gettext utility.
|
||||
"""
|
||||
build_files = []
|
||||
for translatable in files:
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write(
|
||||
"processing file %s in %s"
|
||||
% (translatable.file, translatable.dirpath)
|
||||
)
|
||||
if self.domain not in ("djangojs", "django"):
|
||||
continue
|
||||
build_file = self.build_file_class(self, self.domain, translatable)
|
||||
try:
|
||||
build_file.preprocess()
|
||||
except UnicodeDecodeError as e:
|
||||
self.stdout.write(
|
||||
"UnicodeDecodeError: skipped file %s in %s (reason: %s)"
|
||||
% (
|
||||
translatable.file,
|
||||
translatable.dirpath,
|
||||
e,
|
||||
)
|
||||
)
|
||||
continue
|
||||
except BaseException:
|
||||
# Cleanup before exit.
|
||||
for build_file in build_files:
|
||||
build_file.cleanup()
|
||||
raise
|
||||
build_files.append(build_file)
|
||||
|
||||
if self.domain == "djangojs":
|
||||
args = [
|
||||
"xgettext",
|
||||
"-d",
|
||||
self.domain,
|
||||
"--language=JavaScript",
|
||||
"--keyword=gettext_noop",
|
||||
"--keyword=gettext_lazy",
|
||||
"--keyword=ngettext_lazy:1,2",
|
||||
"--keyword=pgettext:1c,2",
|
||||
"--keyword=npgettext:1c,2,3",
|
||||
"--output=-",
|
||||
]
|
||||
elif self.domain == "django":
|
||||
args = [
|
||||
"xgettext",
|
||||
"-d",
|
||||
self.domain,
|
||||
"--language=Python",
|
||||
"--keyword=gettext_noop",
|
||||
"--keyword=gettext_lazy",
|
||||
"--keyword=ngettext_lazy:1,2",
|
||||
"--keyword=pgettext:1c,2",
|
||||
"--keyword=npgettext:1c,2,3",
|
||||
"--keyword=pgettext_lazy:1c,2",
|
||||
"--keyword=npgettext_lazy:1c,2,3",
|
||||
"--output=-",
|
||||
]
|
||||
else:
|
||||
return
|
||||
|
||||
input_files = [bf.work_path for bf in build_files]
|
||||
with NamedTemporaryFile(mode="w+") as input_files_list:
|
||||
input_files_list.write("\n".join(input_files))
|
||||
input_files_list.flush()
|
||||
args.extend(["--files-from", input_files_list.name])
|
||||
args.extend(self.xgettext_options)
|
||||
msgs, errors, status = popen_wrapper(args)
|
||||
|
||||
if errors:
|
||||
if status != STATUS_OK:
|
||||
for build_file in build_files:
|
||||
build_file.cleanup()
|
||||
raise CommandError(
|
||||
"errors happened while running xgettext on %s\n%s"
|
||||
% ("\n".join(input_files), errors)
|
||||
)
|
||||
elif self.verbosity > 0:
|
||||
# Print warnings
|
||||
self.stdout.write(errors)
|
||||
|
||||
if msgs:
|
||||
if locale_dir is NO_LOCALE_DIR:
|
||||
for build_file in build_files:
|
||||
build_file.cleanup()
|
||||
file_path = os.path.normpath(build_files[0].path)
|
||||
raise CommandError(
|
||||
"Unable to find a locale path to store translations for "
|
||||
"file %s. Make sure the 'locale' directory exists in an "
|
||||
"app or LOCALE_PATHS setting is set." % file_path
|
||||
)
|
||||
for build_file in build_files:
|
||||
msgs = build_file.postprocess_messages(msgs)
|
||||
potfile = os.path.join(locale_dir, "%s.pot" % self.domain)
|
||||
write_pot_file(potfile, msgs)
|
||||
|
||||
for build_file in build_files:
|
||||
build_file.cleanup()
|
||||
|
||||
def write_po_file(self, potfile, locale):
|
||||
"""
|
||||
Create or update the PO file for self.domain and `locale`.
|
||||
Use contents of the existing `potfile`.
|
||||
|
||||
Use msgmerge and msgattrib GNU gettext utilities.
|
||||
"""
|
||||
basedir = os.path.join(os.path.dirname(potfile), locale, "LC_MESSAGES")
|
||||
os.makedirs(basedir, exist_ok=True)
|
||||
pofile = os.path.join(basedir, "%s.po" % self.domain)
|
||||
|
||||
if os.path.exists(pofile):
|
||||
args = ["msgmerge"] + self.msgmerge_options + [pofile, potfile]
|
||||
_, errors, status = popen_wrapper(args)
|
||||
if errors:
|
||||
if status != STATUS_OK:
|
||||
raise CommandError(
|
||||
"errors happened while running msgmerge\n%s" % errors
|
||||
)
|
||||
elif self.verbosity > 0:
|
||||
self.stdout.write(errors)
|
||||
msgs = Path(pofile).read_text(encoding="utf-8")
|
||||
else:
|
||||
with open(potfile, encoding="utf-8") as fp:
|
||||
msgs = fp.read()
|
||||
if not self.invoked_for_django:
|
||||
msgs = self.copy_plural_forms(msgs, locale)
|
||||
msgs = normalize_eols(msgs)
|
||||
msgs = msgs.replace(
|
||||
"#. #-#-#-#-# %s.pot (PACKAGE VERSION) #-#-#-#-#\n" % self.domain, ""
|
||||
)
|
||||
with open(pofile, "w", encoding="utf-8") as fp:
|
||||
fp.write(msgs)
|
||||
|
||||
if self.no_obsolete:
|
||||
args = ["msgattrib"] + self.msgattrib_options + ["-o", pofile, pofile]
|
||||
msgs, errors, status = popen_wrapper(args)
|
||||
if errors:
|
||||
if status != STATUS_OK:
|
||||
raise CommandError(
|
||||
"errors happened while running msgattrib\n%s" % errors
|
||||
)
|
||||
elif self.verbosity > 0:
|
||||
self.stdout.write(errors)
|
||||
|
||||
def copy_plural_forms(self, msgs, locale):
|
||||
"""
|
||||
Copy plural forms header contents from a Django catalog of locale to
|
||||
the msgs string, inserting it at the right place. msgs should be the
|
||||
contents of a newly created .po file.
|
||||
"""
|
||||
django_dir = os.path.normpath(os.path.join(os.path.dirname(django.__file__)))
|
||||
if self.domain == "djangojs":
|
||||
domains = ("djangojs", "django")
|
||||
else:
|
||||
domains = ("django",)
|
||||
for domain in domains:
|
||||
django_po = os.path.join(
|
||||
django_dir, "conf", "locale", locale, "LC_MESSAGES", "%s.po" % domain
|
||||
)
|
||||
if os.path.exists(django_po):
|
||||
with open(django_po, encoding="utf-8") as fp:
|
||||
m = plural_forms_re.search(fp.read())
|
||||
if m:
|
||||
plural_form_line = m["value"]
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write("copying plural forms: %s" % plural_form_line)
|
||||
lines = []
|
||||
found = False
|
||||
for line in msgs.splitlines():
|
||||
if not found and (not line or plural_forms_re.search(line)):
|
||||
line = plural_form_line
|
||||
found = True
|
||||
lines.append(line)
|
||||
msgs = "\n".join(lines)
|
||||
break
|
||||
return msgs
|
||||
@@ -0,0 +1,516 @@
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from itertools import takewhile
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError, no_translations
|
||||
from django.core.management.utils import run_formatters
|
||||
from django.db import DEFAULT_DB_ALIAS, OperationalError, connections, router
|
||||
from django.db.migrations import Migration
|
||||
from django.db.migrations.autodetector import MigrationAutodetector
|
||||
from django.db.migrations.loader import MigrationLoader
|
||||
from django.db.migrations.migration import SwappableTuple
|
||||
from django.db.migrations.optimizer import MigrationOptimizer
|
||||
from django.db.migrations.questioner import (
|
||||
InteractiveMigrationQuestioner,
|
||||
MigrationQuestioner,
|
||||
NonInteractiveMigrationQuestioner,
|
||||
)
|
||||
from django.db.migrations.state import ProjectState
|
||||
from django.db.migrations.utils import get_migration_name_timestamp
|
||||
from django.db.migrations.writer import MigrationWriter
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
autodetector = MigrationAutodetector
|
||||
help = "Creates new migration(s) for apps."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"args",
|
||||
metavar="app_label",
|
||||
nargs="*",
|
||||
help="Specify the app label(s) to create migrations for.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Just show what migrations would be made; don't actually write them.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--merge",
|
||||
action="store_true",
|
||||
help="Enable fixing of migration conflicts.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--empty",
|
||||
action="store_true",
|
||||
help="Create an empty migration.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
help="Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--name",
|
||||
help="Use this name for migration file(s).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-header",
|
||||
action="store_false",
|
||||
dest="include_header",
|
||||
help="Do not add header comments to new migration file(s).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
dest="check_changes",
|
||||
help=(
|
||||
"Exit with a non-zero status if model changes are missing migrations "
|
||||
"and don't actually write them. Implies --dry-run."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scriptable",
|
||||
action="store_true",
|
||||
dest="scriptable",
|
||||
help=(
|
||||
"Divert log output and input prompts to stderr, writing only "
|
||||
"paths of generated migration files to stdout."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--update",
|
||||
action="store_true",
|
||||
dest="update",
|
||||
help=(
|
||||
"Merge model changes into the latest migration and optimize the "
|
||||
"resulting operations."
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def log_output(self):
|
||||
return self.stderr if self.scriptable else self.stdout
|
||||
|
||||
def log(self, msg):
|
||||
self.log_output.write(msg)
|
||||
|
||||
@no_translations
|
||||
def handle(self, *app_labels, **options):
|
||||
self.written_files = []
|
||||
self.verbosity = options["verbosity"]
|
||||
self.interactive = options["interactive"]
|
||||
self.dry_run = options["dry_run"]
|
||||
self.merge = options["merge"]
|
||||
self.empty = options["empty"]
|
||||
self.migration_name = options["name"]
|
||||
if self.migration_name and not self.migration_name.isidentifier():
|
||||
raise CommandError("The migration name must be a valid Python identifier.")
|
||||
self.include_header = options["include_header"]
|
||||
check_changes = options["check_changes"]
|
||||
if check_changes:
|
||||
self.dry_run = True
|
||||
self.scriptable = options["scriptable"]
|
||||
self.update = options["update"]
|
||||
# If logs and prompts are diverted to stderr, remove the ERROR style.
|
||||
if self.scriptable:
|
||||
self.stderr.style_func = None
|
||||
|
||||
# Make sure the app they asked for exists
|
||||
app_labels = set(app_labels)
|
||||
has_bad_labels = False
|
||||
for app_label in app_labels:
|
||||
try:
|
||||
apps.get_app_config(app_label)
|
||||
except LookupError as err:
|
||||
self.stderr.write(str(err))
|
||||
has_bad_labels = True
|
||||
if has_bad_labels:
|
||||
sys.exit(2)
|
||||
|
||||
# Load the current graph state. Pass in None for the connection so
|
||||
# the loader doesn't try to resolve replaced migrations from DB.
|
||||
loader = MigrationLoader(None, ignore_no_migrations=True)
|
||||
|
||||
# Raise an error if any migrations are applied before their dependencies.
|
||||
consistency_check_labels = {config.label for config in apps.get_app_configs()}
|
||||
# Non-default databases are only checked if database routers used.
|
||||
aliases_to_check = (
|
||||
connections if settings.DATABASE_ROUTERS else [DEFAULT_DB_ALIAS]
|
||||
)
|
||||
for alias in sorted(aliases_to_check):
|
||||
connection = connections[alias]
|
||||
if connection.settings_dict["ENGINE"] != "django.db.backends.dummy" and any(
|
||||
# At least one model must be migrated to the database.
|
||||
router.allow_migrate(
|
||||
connection.alias, app_label, model_name=model._meta.object_name
|
||||
)
|
||||
for app_label in consistency_check_labels
|
||||
for model in apps.get_app_config(app_label).get_models()
|
||||
):
|
||||
try:
|
||||
loader.check_consistent_history(connection)
|
||||
except OperationalError as error:
|
||||
warnings.warn(
|
||||
"Got an error checking a consistent migration history "
|
||||
"performed for database connection '%s': %s" % (alias, error),
|
||||
RuntimeWarning,
|
||||
)
|
||||
# Before anything else, see if there's conflicting apps and drop out
|
||||
# hard if there are any and they don't want to merge
|
||||
conflicts = loader.detect_conflicts()
|
||||
|
||||
# If app_labels is specified, filter out conflicting migrations for
|
||||
# unspecified apps.
|
||||
if app_labels:
|
||||
conflicts = {
|
||||
app_label: conflict
|
||||
for app_label, conflict in conflicts.items()
|
||||
if app_label in app_labels
|
||||
}
|
||||
|
||||
if conflicts and not self.merge:
|
||||
name_str = "; ".join(
|
||||
"%s in %s" % (", ".join(names), app) for app, names in conflicts.items()
|
||||
)
|
||||
raise CommandError(
|
||||
"Conflicting migrations detected; multiple leaf nodes in the "
|
||||
"migration graph: (%s).\nTo fix them run "
|
||||
"'python manage.py makemigrations --merge'" % name_str
|
||||
)
|
||||
|
||||
# If they want to merge and there's nothing to merge, then politely exit
|
||||
if self.merge and not conflicts:
|
||||
self.log("No conflicts detected to merge.")
|
||||
return
|
||||
|
||||
# If they want to merge and there is something to merge, then
|
||||
# divert into the merge code
|
||||
if self.merge and conflicts:
|
||||
return self.handle_merge(loader, conflicts)
|
||||
|
||||
if self.interactive:
|
||||
questioner = InteractiveMigrationQuestioner(
|
||||
specified_apps=app_labels,
|
||||
dry_run=self.dry_run,
|
||||
prompt_output=self.log_output,
|
||||
)
|
||||
else:
|
||||
questioner = NonInteractiveMigrationQuestioner(
|
||||
specified_apps=app_labels,
|
||||
dry_run=self.dry_run,
|
||||
verbosity=self.verbosity,
|
||||
log=self.log,
|
||||
)
|
||||
# Set up autodetector
|
||||
autodetector = self.autodetector(
|
||||
loader.project_state(),
|
||||
ProjectState.from_apps(apps),
|
||||
questioner,
|
||||
)
|
||||
|
||||
# If they want to make an empty migration, make one for each app
|
||||
if self.empty:
|
||||
if not app_labels:
|
||||
raise CommandError(
|
||||
"You must supply at least one app label when using --empty."
|
||||
)
|
||||
# Make a fake changes() result we can pass to arrange_for_graph
|
||||
changes = {app: [Migration("custom", app)] for app in app_labels}
|
||||
changes = autodetector.arrange_for_graph(
|
||||
changes=changes,
|
||||
graph=loader.graph,
|
||||
migration_name=self.migration_name,
|
||||
)
|
||||
self.write_migration_files(changes)
|
||||
return
|
||||
|
||||
# Detect changes
|
||||
changes = autodetector.changes(
|
||||
graph=loader.graph,
|
||||
trim_to_apps=app_labels or None,
|
||||
convert_apps=app_labels or None,
|
||||
migration_name=self.migration_name,
|
||||
)
|
||||
|
||||
if not changes:
|
||||
# No changes? Tell them.
|
||||
if self.verbosity >= 1:
|
||||
if app_labels:
|
||||
if len(app_labels) == 1:
|
||||
self.log("No changes detected in app '%s'" % app_labels.pop())
|
||||
else:
|
||||
self.log(
|
||||
"No changes detected in apps '%s'"
|
||||
% ("', '".join(app_labels))
|
||||
)
|
||||
else:
|
||||
self.log("No changes detected")
|
||||
else:
|
||||
if self.update:
|
||||
self.write_to_last_migration_files(changes)
|
||||
else:
|
||||
self.write_migration_files(changes)
|
||||
if check_changes:
|
||||
sys.exit(1)
|
||||
|
||||
def write_to_last_migration_files(self, changes):
|
||||
loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
|
||||
new_changes = {}
|
||||
update_previous_migration_paths = {}
|
||||
for app_label, app_migrations in changes.items():
|
||||
# Find last migration.
|
||||
leaf_migration_nodes = loader.graph.leaf_nodes(app=app_label)
|
||||
if len(leaf_migration_nodes) == 0:
|
||||
raise CommandError(
|
||||
f"App {app_label} has no migration, cannot update last migration."
|
||||
)
|
||||
leaf_migration_node = leaf_migration_nodes[0]
|
||||
# Multiple leaf nodes have already been checked earlier in command.
|
||||
leaf_migration = loader.graph.nodes[leaf_migration_node]
|
||||
# Updated migration cannot be a squash migration, a dependency of
|
||||
# another migration, and cannot be already applied.
|
||||
if leaf_migration.replaces:
|
||||
raise CommandError(
|
||||
f"Cannot update squash migration '{leaf_migration}'."
|
||||
)
|
||||
if leaf_migration_node in loader.applied_migrations:
|
||||
raise CommandError(
|
||||
f"Cannot update applied migration '{leaf_migration}'."
|
||||
)
|
||||
depending_migrations = [
|
||||
migration
|
||||
for migration in loader.disk_migrations.values()
|
||||
if leaf_migration_node in migration.dependencies
|
||||
]
|
||||
if depending_migrations:
|
||||
formatted_migrations = ", ".join(
|
||||
[f"'{migration}'" for migration in depending_migrations]
|
||||
)
|
||||
raise CommandError(
|
||||
f"Cannot update migration '{leaf_migration}' that migrations "
|
||||
f"{formatted_migrations} depend on."
|
||||
)
|
||||
# Build new migration.
|
||||
for migration in app_migrations:
|
||||
leaf_migration.operations.extend(migration.operations)
|
||||
|
||||
for dependency in migration.dependencies:
|
||||
if isinstance(dependency, SwappableTuple):
|
||||
if settings.AUTH_USER_MODEL == dependency.setting:
|
||||
leaf_migration.dependencies.append(
|
||||
("__setting__", "AUTH_USER_MODEL")
|
||||
)
|
||||
else:
|
||||
leaf_migration.dependencies.append(dependency)
|
||||
elif dependency[0] != migration.app_label:
|
||||
leaf_migration.dependencies.append(dependency)
|
||||
# Optimize migration.
|
||||
optimizer = MigrationOptimizer()
|
||||
leaf_migration.operations = optimizer.optimize(
|
||||
leaf_migration.operations, app_label
|
||||
)
|
||||
# Update name.
|
||||
previous_migration_path = MigrationWriter(leaf_migration).path
|
||||
name_fragment = self.migration_name or leaf_migration.suggest_name()
|
||||
suggested_name = leaf_migration.name[:4] + f"_{name_fragment}"
|
||||
if leaf_migration.name == suggested_name:
|
||||
new_name = leaf_migration.name + "_updated"
|
||||
else:
|
||||
new_name = suggested_name
|
||||
leaf_migration.name = new_name
|
||||
# Register overridden migration.
|
||||
new_changes[app_label] = [leaf_migration]
|
||||
update_previous_migration_paths[app_label] = previous_migration_path
|
||||
|
||||
self.write_migration_files(new_changes, update_previous_migration_paths)
|
||||
|
||||
def write_migration_files(self, changes, update_previous_migration_paths=None):
|
||||
"""
|
||||
Take a changes dict and write them out as migration files.
|
||||
"""
|
||||
directory_created = {}
|
||||
for app_label, app_migrations in changes.items():
|
||||
if self.verbosity >= 1:
|
||||
self.log(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label))
|
||||
for migration in app_migrations:
|
||||
# Describe the migration
|
||||
writer = MigrationWriter(migration, self.include_header)
|
||||
if self.verbosity >= 1:
|
||||
# Display a relative path if it's below the current working
|
||||
# directory, or an absolute path otherwise.
|
||||
migration_string = self.get_relative_path(writer.path)
|
||||
self.log(" %s\n" % self.style.MIGRATE_LABEL(migration_string))
|
||||
for operation in migration.operations:
|
||||
self.log(" %s" % operation.formatted_description())
|
||||
if self.scriptable:
|
||||
self.stdout.write(migration_string)
|
||||
if not self.dry_run:
|
||||
# Write the migrations file to the disk.
|
||||
migrations_directory = os.path.dirname(writer.path)
|
||||
if not directory_created.get(app_label):
|
||||
os.makedirs(migrations_directory, exist_ok=True)
|
||||
init_path = os.path.join(migrations_directory, "__init__.py")
|
||||
if not os.path.isfile(init_path):
|
||||
open(init_path, "w").close()
|
||||
# We just do this once per app
|
||||
directory_created[app_label] = True
|
||||
migration_string = writer.as_string()
|
||||
with open(writer.path, "w", encoding="utf-8") as fh:
|
||||
fh.write(migration_string)
|
||||
self.written_files.append(writer.path)
|
||||
if update_previous_migration_paths:
|
||||
prev_path = update_previous_migration_paths[app_label]
|
||||
rel_prev_path = self.get_relative_path(prev_path)
|
||||
if writer.needs_manual_porting:
|
||||
migration_path = self.get_relative_path(writer.path)
|
||||
self.log(
|
||||
self.style.WARNING(
|
||||
f"Updated migration {migration_path} requires "
|
||||
f"manual porting.\n"
|
||||
f"Previous migration {rel_prev_path} was kept and "
|
||||
f"must be deleted after porting functions manually."
|
||||
)
|
||||
)
|
||||
else:
|
||||
os.remove(prev_path)
|
||||
self.log(f"Deleted {rel_prev_path}")
|
||||
elif self.verbosity == 3:
|
||||
# Alternatively, makemigrations --dry-run --verbosity 3
|
||||
# will log the migrations rather than saving the file to
|
||||
# the disk.
|
||||
self.log(
|
||||
self.style.MIGRATE_HEADING(
|
||||
"Full migrations file '%s':" % writer.filename
|
||||
)
|
||||
)
|
||||
self.log(writer.as_string())
|
||||
run_formatters(self.written_files, stderr=self.stderr)
|
||||
|
||||
@staticmethod
|
||||
def get_relative_path(path):
|
||||
try:
|
||||
migration_string = os.path.relpath(path)
|
||||
except ValueError:
|
||||
migration_string = path
|
||||
if migration_string.startswith(".."):
|
||||
migration_string = path
|
||||
return migration_string
|
||||
|
||||
def handle_merge(self, loader, conflicts):
|
||||
"""
|
||||
Handles merging together conflicted migrations interactively,
|
||||
if it's safe; otherwise, advises on how to fix it.
|
||||
"""
|
||||
if self.interactive:
|
||||
questioner = InteractiveMigrationQuestioner(prompt_output=self.log_output)
|
||||
else:
|
||||
questioner = MigrationQuestioner(defaults={"ask_merge": True})
|
||||
|
||||
for app_label, migration_names in conflicts.items():
|
||||
# Grab out the migrations in question, and work out their
|
||||
# common ancestor.
|
||||
merge_migrations = []
|
||||
for migration_name in migration_names:
|
||||
migration = loader.get_migration(app_label, migration_name)
|
||||
migration.ancestry = [
|
||||
mig
|
||||
for mig in loader.graph.forwards_plan((app_label, migration_name))
|
||||
if mig[0] == migration.app_label
|
||||
]
|
||||
merge_migrations.append(migration)
|
||||
|
||||
def all_items_equal(seq):
|
||||
return all(item == seq[0] for item in seq[1:])
|
||||
|
||||
merge_migrations_generations = zip(*(m.ancestry for m in merge_migrations))
|
||||
common_ancestor_count = sum(
|
||||
1
|
||||
for common_ancestor_generation in takewhile(
|
||||
all_items_equal, merge_migrations_generations
|
||||
)
|
||||
)
|
||||
if not common_ancestor_count:
|
||||
raise ValueError(
|
||||
"Could not find common ancestor of %s" % migration_names
|
||||
)
|
||||
# Now work out the operations along each divergent branch
|
||||
for migration in merge_migrations:
|
||||
migration.branch = migration.ancestry[common_ancestor_count:]
|
||||
migrations_ops = (
|
||||
loader.get_migration(node_app, node_name).operations
|
||||
for node_app, node_name in migration.branch
|
||||
)
|
||||
migration.merged_operations = sum(migrations_ops, [])
|
||||
# In future, this could use some of the Optimizer code
|
||||
# (can_optimize_through) to automatically see if they're
|
||||
# mergeable. For now, we always just prompt the user.
|
||||
if self.verbosity > 0:
|
||||
self.log(self.style.MIGRATE_HEADING("Merging %s" % app_label))
|
||||
for migration in merge_migrations:
|
||||
self.log(self.style.MIGRATE_LABEL(" Branch %s" % migration.name))
|
||||
for operation in migration.merged_operations:
|
||||
self.log(" %s" % operation.formatted_description())
|
||||
if questioner.ask_merge(app_label):
|
||||
# If they still want to merge it, then write out an empty
|
||||
# file depending on the migrations needing merging.
|
||||
numbers = [
|
||||
self.autodetector.parse_number(migration.name)
|
||||
for migration in merge_migrations
|
||||
]
|
||||
try:
|
||||
biggest_number = max(x for x in numbers if x is not None)
|
||||
except ValueError:
|
||||
biggest_number = 1
|
||||
subclass = type(
|
||||
"Migration",
|
||||
(Migration,),
|
||||
{
|
||||
"dependencies": [
|
||||
(app_label, migration.name)
|
||||
for migration in merge_migrations
|
||||
],
|
||||
},
|
||||
)
|
||||
parts = ["%04i" % (biggest_number + 1)]
|
||||
if self.migration_name:
|
||||
parts.append(self.migration_name)
|
||||
else:
|
||||
parts.append("merge")
|
||||
leaf_names = "_".join(
|
||||
sorted(migration.name for migration in merge_migrations)
|
||||
)
|
||||
if len(leaf_names) > 47:
|
||||
parts.append(get_migration_name_timestamp())
|
||||
else:
|
||||
parts.append(leaf_names)
|
||||
migration_name = "_".join(parts)
|
||||
new_migration = subclass(migration_name, app_label)
|
||||
writer = MigrationWriter(new_migration, self.include_header)
|
||||
|
||||
if not self.dry_run:
|
||||
# Write the merge migrations file to the disk
|
||||
with open(writer.path, "w", encoding="utf-8") as fh:
|
||||
fh.write(writer.as_string())
|
||||
run_formatters([writer.path], stderr=self.stderr)
|
||||
if self.verbosity > 0:
|
||||
self.log("\nCreated new merge migration %s" % writer.path)
|
||||
if self.scriptable:
|
||||
self.stdout.write(writer.path)
|
||||
elif self.verbosity == 3:
|
||||
# Alternatively, makemigrations --merge --dry-run --verbosity 3
|
||||
# will log the merge migrations rather than saving the file
|
||||
# to the disk.
|
||||
self.log(
|
||||
self.style.MIGRATE_HEADING(
|
||||
"Full merge migrations file '%s':" % writer.filename
|
||||
)
|
||||
)
|
||||
self.log(writer.as_string())
|
||||
@@ -0,0 +1,508 @@
|
||||
import sys
|
||||
import time
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, CommandError, no_translations
|
||||
from django.core.management.sql import emit_post_migrate_signal, emit_pre_migrate_signal
|
||||
from django.db import DEFAULT_DB_ALIAS, connections, router
|
||||
from django.db.migrations.autodetector import MigrationAutodetector
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
from django.db.migrations.loader import AmbiguityError
|
||||
from django.db.migrations.state import ModelState, ProjectState
|
||||
from django.utils.module_loading import module_has_submodule
|
||||
from django.utils.text import Truncator
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
autodetector = MigrationAutodetector
|
||||
help = (
|
||||
"Updates database schema. Manages both apps with migrations and those without."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"app_label",
|
||||
nargs="?",
|
||||
help="App label of an application to synchronize the state.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"migration_name",
|
||||
nargs="?",
|
||||
help="Database state will be brought to the state after that "
|
||||
'migration. Use the name "zero" to unapply all migrations.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
help="Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help=(
|
||||
'Nominates a database to synchronize. Defaults to the "default" '
|
||||
"database."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fake",
|
||||
action="store_true",
|
||||
help="Mark migrations as run without actually running them.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fake-initial",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Detect if tables already exist and fake-apply initial migrations if "
|
||||
"so. Make sure that the current database schema matches your initial "
|
||||
"migration before using this flag. Django will only check for an "
|
||||
"existing table name."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plan",
|
||||
action="store_true",
|
||||
help="Shows a list of the migration actions that will be performed.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run-syncdb",
|
||||
action="store_true",
|
||||
help="Creates tables for apps without migrations.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
dest="check_unapplied",
|
||||
help=(
|
||||
"Exits with a non-zero status if unapplied migrations exist and does "
|
||||
"not actually apply migrations."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prune",
|
||||
action="store_true",
|
||||
dest="prune",
|
||||
help="Delete nonexistent migrations from the django_migrations table.",
|
||||
)
|
||||
|
||||
def get_check_kwargs(self, options):
|
||||
kwargs = super().get_check_kwargs(options)
|
||||
return {**kwargs, "databases": [options["database"]]}
|
||||
|
||||
@no_translations
|
||||
def handle(self, *args, **options):
|
||||
database = options["database"]
|
||||
self.verbosity = options["verbosity"]
|
||||
self.interactive = options["interactive"]
|
||||
|
||||
# Import the 'management' module within each installed app, to register
|
||||
# dispatcher events.
|
||||
for app_config in apps.get_app_configs():
|
||||
if module_has_submodule(app_config.module, "management"):
|
||||
import_module(".management", app_config.name)
|
||||
|
||||
# Get the database we're operating from
|
||||
connection = connections[database]
|
||||
|
||||
# Hook for backends needing any database preparation
|
||||
connection.prepare_database()
|
||||
# Work out which apps have migrations and which do not
|
||||
executor = MigrationExecutor(connection, self.migration_progress_callback)
|
||||
|
||||
# Raise an error if any migrations are applied before their dependencies.
|
||||
executor.loader.check_consistent_history(connection)
|
||||
|
||||
# Before anything else, see if there's conflicting apps and drop out
|
||||
# hard if there are any
|
||||
conflicts = executor.loader.detect_conflicts()
|
||||
if conflicts:
|
||||
name_str = "; ".join(
|
||||
"%s in %s" % (", ".join(names), app) for app, names in conflicts.items()
|
||||
)
|
||||
raise CommandError(
|
||||
"Conflicting migrations detected; multiple leaf nodes in the "
|
||||
"migration graph: (%s).\nTo fix them run "
|
||||
"'python manage.py makemigrations --merge'" % name_str
|
||||
)
|
||||
|
||||
# If they supplied command line arguments, work out what they mean.
|
||||
run_syncdb = options["run_syncdb"]
|
||||
target_app_labels_only = True
|
||||
if options["app_label"]:
|
||||
# Validate app_label.
|
||||
app_label = options["app_label"]
|
||||
try:
|
||||
apps.get_app_config(app_label)
|
||||
except LookupError as err:
|
||||
raise CommandError(str(err))
|
||||
if run_syncdb:
|
||||
if app_label in executor.loader.migrated_apps:
|
||||
raise CommandError(
|
||||
"Can't use run_syncdb with app '%s' as it has migrations."
|
||||
% app_label
|
||||
)
|
||||
elif app_label not in executor.loader.migrated_apps:
|
||||
raise CommandError("App '%s' does not have migrations." % app_label)
|
||||
|
||||
if options["app_label"] and options["migration_name"]:
|
||||
migration_name = options["migration_name"]
|
||||
if migration_name == "zero":
|
||||
targets = [(app_label, None)]
|
||||
else:
|
||||
try:
|
||||
migration = executor.loader.get_migration_by_prefix(
|
||||
app_label, migration_name
|
||||
)
|
||||
except AmbiguityError:
|
||||
raise CommandError(
|
||||
"More than one migration matches '%s' in app '%s'. "
|
||||
"Please be more specific." % (migration_name, app_label)
|
||||
)
|
||||
except KeyError:
|
||||
raise CommandError(
|
||||
"Cannot find a migration matching '%s' from app '%s'."
|
||||
% (migration_name, app_label)
|
||||
)
|
||||
target = (app_label, migration.name)
|
||||
# Partially applied squashed migrations are not included in the
|
||||
# graph, use the last replacement instead.
|
||||
if (
|
||||
target not in executor.loader.graph.nodes
|
||||
and target in executor.loader.replacements
|
||||
):
|
||||
incomplete_migration = executor.loader.replacements[target]
|
||||
target = incomplete_migration.replaces[-1]
|
||||
targets = [target]
|
||||
target_app_labels_only = False
|
||||
elif options["app_label"]:
|
||||
targets = [
|
||||
key for key in executor.loader.graph.leaf_nodes() if key[0] == app_label
|
||||
]
|
||||
else:
|
||||
targets = executor.loader.graph.leaf_nodes()
|
||||
|
||||
if options["prune"]:
|
||||
if not options["app_label"]:
|
||||
raise CommandError(
|
||||
"Migrations can be pruned only when an app is specified."
|
||||
)
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write("Pruning migrations:", self.style.MIGRATE_HEADING)
|
||||
to_prune = sorted(
|
||||
migration
|
||||
for migration in set(executor.loader.applied_migrations)
|
||||
- set(executor.loader.disk_migrations)
|
||||
if migration[0] == app_label
|
||||
)
|
||||
squashed_migrations_with_deleted_replaced_migrations = [
|
||||
migration_key
|
||||
for migration_key, migration_obj in executor.loader.replacements.items()
|
||||
if any(replaced in to_prune for replaced in migration_obj.replaces)
|
||||
]
|
||||
if squashed_migrations_with_deleted_replaced_migrations:
|
||||
self.stdout.write(
|
||||
self.style.NOTICE(
|
||||
" Cannot use --prune because the following squashed "
|
||||
"migrations have their 'replaces' attributes and may not "
|
||||
"be recorded as applied:"
|
||||
)
|
||||
)
|
||||
for migration in squashed_migrations_with_deleted_replaced_migrations:
|
||||
app, name = migration
|
||||
self.stdout.write(f" {app}.{name}")
|
||||
self.stdout.write(
|
||||
self.style.NOTICE(
|
||||
" Re-run 'manage.py migrate' if they are not marked as "
|
||||
"applied, and remove 'replaces' attributes in their "
|
||||
"Migration classes."
|
||||
)
|
||||
)
|
||||
else:
|
||||
if to_prune:
|
||||
for migration in to_prune:
|
||||
app, name = migration
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_LABEL(f" Pruning {app}.{name}"),
|
||||
ending="",
|
||||
)
|
||||
executor.recorder.record_unapplied(app, name)
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write(self.style.SUCCESS(" OK"))
|
||||
elif self.verbosity > 0:
|
||||
self.stdout.write(" No migrations to prune.")
|
||||
|
||||
plan = executor.migration_plan(targets)
|
||||
|
||||
if options["plan"]:
|
||||
self.stdout.write("Planned operations:", self.style.MIGRATE_LABEL)
|
||||
if not plan:
|
||||
self.stdout.write(" No planned migration operations.")
|
||||
else:
|
||||
for migration, backwards in plan:
|
||||
self.stdout.write(str(migration), self.style.MIGRATE_HEADING)
|
||||
for operation in migration.operations:
|
||||
message, is_error = self.describe_operation(
|
||||
operation, backwards
|
||||
)
|
||||
style = self.style.WARNING if is_error else None
|
||||
self.stdout.write(" " + message, style)
|
||||
if options["check_unapplied"]:
|
||||
sys.exit(1)
|
||||
return
|
||||
if options["check_unapplied"]:
|
||||
if plan:
|
||||
sys.exit(1)
|
||||
return
|
||||
if options["prune"]:
|
||||
return
|
||||
|
||||
# At this point, ignore run_syncdb if there aren't any apps to sync.
|
||||
run_syncdb = options["run_syncdb"] and executor.loader.unmigrated_apps
|
||||
# Print some useful info
|
||||
if self.verbosity >= 1:
|
||||
self.stdout.write(self.style.MIGRATE_HEADING("Operations to perform:"))
|
||||
if run_syncdb:
|
||||
if options["app_label"]:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_LABEL(
|
||||
" Synchronize unmigrated app: %s" % app_label
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_LABEL(" Synchronize unmigrated apps: ")
|
||||
+ (", ".join(sorted(executor.loader.unmigrated_apps)))
|
||||
)
|
||||
if target_app_labels_only:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_LABEL(" Apply all migrations: ")
|
||||
+ (", ".join(sorted({a for a, n in targets})) or "(none)")
|
||||
)
|
||||
else:
|
||||
if targets[0][1] is None:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_LABEL(" Unapply all migrations: ")
|
||||
+ str(targets[0][0])
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_LABEL(" Target specific migration: ")
|
||||
+ "%s, from %s" % (targets[0][1], targets[0][0])
|
||||
)
|
||||
|
||||
pre_migrate_state = executor._create_project_state(with_applied_migrations=True)
|
||||
pre_migrate_apps = pre_migrate_state.apps
|
||||
emit_pre_migrate_signal(
|
||||
self.verbosity,
|
||||
self.interactive,
|
||||
connection.alias,
|
||||
stdout=self.stdout,
|
||||
apps=pre_migrate_apps,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
# Run the syncdb phase.
|
||||
if run_syncdb:
|
||||
if self.verbosity >= 1:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING("Synchronizing apps without migrations:")
|
||||
)
|
||||
if options["app_label"]:
|
||||
self.sync_apps(connection, [app_label])
|
||||
else:
|
||||
self.sync_apps(connection, executor.loader.unmigrated_apps)
|
||||
|
||||
# Migrate!
|
||||
if self.verbosity >= 1:
|
||||
self.stdout.write(self.style.MIGRATE_HEADING("Running migrations:"))
|
||||
if not plan:
|
||||
if self.verbosity >= 1:
|
||||
self.stdout.write(" No migrations to apply.")
|
||||
# If there's changes that aren't in migrations yet, tell them
|
||||
# how to fix it.
|
||||
autodetector = self.autodetector(
|
||||
executor.loader.project_state(),
|
||||
ProjectState.from_apps(apps),
|
||||
)
|
||||
changes = autodetector.changes(graph=executor.loader.graph)
|
||||
if changes:
|
||||
self.stdout.write(
|
||||
self.style.NOTICE(
|
||||
" Your models in app(s): %s have changes that are not "
|
||||
"yet reflected in a migration, and so won't be "
|
||||
"applied." % ", ".join(repr(app) for app in sorted(changes))
|
||||
)
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.NOTICE(
|
||||
" Run 'manage.py makemigrations' to make new "
|
||||
"migrations, and then re-run 'manage.py migrate' to "
|
||||
"apply them."
|
||||
)
|
||||
)
|
||||
fake = False
|
||||
fake_initial = False
|
||||
else:
|
||||
fake = options["fake"]
|
||||
fake_initial = options["fake_initial"]
|
||||
post_migrate_state = executor.migrate(
|
||||
targets,
|
||||
plan=plan,
|
||||
state=pre_migrate_state.clone(),
|
||||
fake=fake,
|
||||
fake_initial=fake_initial,
|
||||
)
|
||||
# post_migrate signals have access to all models. Ensure that all models
|
||||
# are reloaded in case any are delayed.
|
||||
post_migrate_state.clear_delayed_apps_cache()
|
||||
post_migrate_apps = post_migrate_state.apps
|
||||
|
||||
# Re-render models of real apps to include relationships now that
|
||||
# we've got a final state. This wouldn't be necessary if real apps
|
||||
# models were rendered with relationships in the first place.
|
||||
with post_migrate_apps.bulk_update():
|
||||
model_keys = []
|
||||
for model_state in post_migrate_apps.real_models:
|
||||
model_key = model_state.app_label, model_state.name_lower
|
||||
model_keys.append(model_key)
|
||||
post_migrate_apps.unregister_model(*model_key)
|
||||
post_migrate_apps.render_multiple(
|
||||
[ModelState.from_model(apps.get_model(*model)) for model in model_keys]
|
||||
)
|
||||
|
||||
# Send the post_migrate signal, so individual apps can do whatever they need
|
||||
# to do at this point.
|
||||
emit_post_migrate_signal(
|
||||
self.verbosity,
|
||||
self.interactive,
|
||||
connection.alias,
|
||||
stdout=self.stdout,
|
||||
apps=post_migrate_apps,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
def migration_progress_callback(self, action, migration=None, fake=False):
|
||||
if self.verbosity >= 1:
|
||||
compute_time = self.verbosity > 1
|
||||
if action == "apply_start":
|
||||
if compute_time:
|
||||
self.start = time.monotonic()
|
||||
self.stdout.write(" Applying %s..." % migration, ending="")
|
||||
self.stdout.flush()
|
||||
elif action == "apply_success":
|
||||
elapsed = (
|
||||
" (%.3fs)" % (time.monotonic() - self.start) if compute_time else ""
|
||||
)
|
||||
if fake:
|
||||
self.stdout.write(self.style.SUCCESS(" FAKED" + elapsed))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(" OK" + elapsed))
|
||||
elif action == "unapply_start":
|
||||
if compute_time:
|
||||
self.start = time.monotonic()
|
||||
self.stdout.write(" Unapplying %s..." % migration, ending="")
|
||||
self.stdout.flush()
|
||||
elif action == "unapply_success":
|
||||
elapsed = (
|
||||
" (%.3fs)" % (time.monotonic() - self.start) if compute_time else ""
|
||||
)
|
||||
if fake:
|
||||
self.stdout.write(self.style.SUCCESS(" FAKED" + elapsed))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(" OK" + elapsed))
|
||||
elif action == "render_start":
|
||||
if compute_time:
|
||||
self.start = time.monotonic()
|
||||
self.stdout.write(" Rendering model states...", ending="")
|
||||
self.stdout.flush()
|
||||
elif action == "render_success":
|
||||
elapsed = (
|
||||
" (%.3fs)" % (time.monotonic() - self.start) if compute_time else ""
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(" DONE" + elapsed))
|
||||
|
||||
def sync_apps(self, connection, app_labels):
|
||||
"""Run the old syncdb-style operation on a list of app_labels."""
|
||||
with connection.cursor() as cursor:
|
||||
tables = connection.introspection.table_names(cursor)
|
||||
|
||||
# Build the manifest of apps and models that are to be synchronized.
|
||||
all_models = [
|
||||
(
|
||||
app_config.label,
|
||||
router.get_migratable_models(
|
||||
app_config, connection.alias, include_auto_created=False
|
||||
),
|
||||
)
|
||||
for app_config in apps.get_app_configs()
|
||||
if app_config.models_module is not None and app_config.label in app_labels
|
||||
]
|
||||
|
||||
def model_installed(model):
|
||||
opts = model._meta
|
||||
converter = connection.introspection.identifier_converter
|
||||
return not (
|
||||
(converter(opts.db_table) in tables)
|
||||
or (
|
||||
opts.auto_created
|
||||
and converter(opts.auto_created._meta.db_table) in tables
|
||||
)
|
||||
)
|
||||
|
||||
manifest = {
|
||||
app_name: list(filter(model_installed, model_list))
|
||||
for app_name, model_list in all_models
|
||||
}
|
||||
|
||||
# Create the tables for each model
|
||||
if self.verbosity >= 1:
|
||||
self.stdout.write(" Creating tables...")
|
||||
with connection.schema_editor() as editor:
|
||||
for app_name, model_list in manifest.items():
|
||||
for model in model_list:
|
||||
# Never install unmanaged models, etc.
|
||||
if not model._meta.can_migrate(connection):
|
||||
continue
|
||||
if self.verbosity >= 3:
|
||||
self.stdout.write(
|
||||
" Processing %s.%s model"
|
||||
% (app_name, model._meta.object_name)
|
||||
)
|
||||
if self.verbosity >= 1:
|
||||
self.stdout.write(
|
||||
" Creating table %s" % model._meta.db_table
|
||||
)
|
||||
editor.create_model(model)
|
||||
|
||||
# Deferred SQL is executed when exiting the editor's context.
|
||||
if self.verbosity >= 1:
|
||||
self.stdout.write(" Running deferred SQL...")
|
||||
|
||||
@staticmethod
|
||||
def describe_operation(operation, backwards):
|
||||
"""Return a string that describes a migration operation for --plan."""
|
||||
prefix = ""
|
||||
is_error = False
|
||||
if hasattr(operation, "code"):
|
||||
code = operation.reverse_code if backwards else operation.code
|
||||
action = (code.__doc__ or "") if code else None
|
||||
elif hasattr(operation, "sql"):
|
||||
action = operation.reverse_sql if backwards else operation.sql
|
||||
else:
|
||||
action = ""
|
||||
if backwards:
|
||||
prefix = "Undo "
|
||||
if action is not None:
|
||||
action = str(action).replace("\n", "")
|
||||
elif backwards:
|
||||
action = "IRREVERSIBLE"
|
||||
is_error = True
|
||||
if action:
|
||||
action = " -> " + action
|
||||
truncated = Truncator(action)
|
||||
return prefix + operation.describe() + truncated.chars(40), is_error
|
||||
@@ -0,0 +1,129 @@
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.utils import run_formatters
|
||||
from django.db import migrations
|
||||
from django.db.migrations.exceptions import AmbiguityError
|
||||
from django.db.migrations.loader import MigrationLoader
|
||||
from django.db.migrations.optimizer import MigrationOptimizer
|
||||
from django.db.migrations.writer import MigrationWriter
|
||||
from django.utils.version import get_docs_version
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Optimizes the operations for the named migration."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"app_label",
|
||||
help="App label of the application to optimize the migration for.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"migration_name", help="Migration name to optimize the operations for."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Exit with a non-zero status if the migration can be optimized.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbosity = options["verbosity"]
|
||||
app_label = options["app_label"]
|
||||
migration_name = options["migration_name"]
|
||||
check = options["check"]
|
||||
|
||||
# Validate app_label.
|
||||
try:
|
||||
apps.get_app_config(app_label)
|
||||
except LookupError as err:
|
||||
raise CommandError(str(err))
|
||||
|
||||
# Load the current graph state.
|
||||
loader = MigrationLoader(None)
|
||||
if app_label not in loader.migrated_apps:
|
||||
raise CommandError(f"App '{app_label}' does not have migrations.")
|
||||
# Find a migration.
|
||||
try:
|
||||
migration = loader.get_migration_by_prefix(app_label, migration_name)
|
||||
except AmbiguityError:
|
||||
raise CommandError(
|
||||
f"More than one migration matches '{migration_name}' in app "
|
||||
f"'{app_label}'. Please be more specific."
|
||||
)
|
||||
except KeyError:
|
||||
raise CommandError(
|
||||
f"Cannot find a migration matching '{migration_name}' from app "
|
||||
f"'{app_label}'."
|
||||
)
|
||||
|
||||
# Optimize the migration.
|
||||
optimizer = MigrationOptimizer()
|
||||
new_operations = optimizer.optimize(migration.operations, migration.app_label)
|
||||
if len(migration.operations) == len(new_operations):
|
||||
if verbosity > 0:
|
||||
self.stdout.write("No optimizations possible.")
|
||||
return
|
||||
else:
|
||||
if verbosity > 0:
|
||||
self.stdout.write(
|
||||
"Optimizing from %d operations to %d operations."
|
||||
% (len(migration.operations), len(new_operations))
|
||||
)
|
||||
if check:
|
||||
sys.exit(1)
|
||||
|
||||
# Set the new migration optimizations.
|
||||
migration.operations = new_operations
|
||||
|
||||
# Write out the optimized migration file.
|
||||
writer = MigrationWriter(migration)
|
||||
migration_file_string = writer.as_string()
|
||||
if writer.needs_manual_porting:
|
||||
if migration.replaces:
|
||||
raise CommandError(
|
||||
"Migration will require manual porting but is already a squashed "
|
||||
"migration.\nTransition to a normal migration first: "
|
||||
"https://docs.djangoproject.com/en/%s/topics/migrations/"
|
||||
"#squashing-migrations" % get_docs_version()
|
||||
)
|
||||
# Make a new migration with those operations.
|
||||
subclass = type(
|
||||
"Migration",
|
||||
(migrations.Migration,),
|
||||
{
|
||||
"dependencies": migration.dependencies,
|
||||
"operations": new_operations,
|
||||
"replaces": [(migration.app_label, migration.name)],
|
||||
},
|
||||
)
|
||||
optimized_migration_name = "%s_optimized" % migration.name
|
||||
optimized_migration = subclass(optimized_migration_name, app_label)
|
||||
writer = MigrationWriter(optimized_migration)
|
||||
migration_file_string = writer.as_string()
|
||||
if verbosity > 0:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING("Manual porting required") + "\n"
|
||||
" Your migrations contained functions that must be manually "
|
||||
"copied over,\n"
|
||||
" as we could not safely copy their implementation.\n"
|
||||
" See the comment at the top of the optimized migration for "
|
||||
"details."
|
||||
)
|
||||
if shutil.which("black"):
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"Optimized migration couldn't be formatted using the "
|
||||
'"black" command. You can call it manually.'
|
||||
)
|
||||
)
|
||||
with open(writer.path, "w", encoding="utf-8") as fh:
|
||||
fh.write(migration_file_string)
|
||||
run_formatters([writer.path], stderr=self.stderr)
|
||||
|
||||
if verbosity > 0:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING(f"Optimized migration {writer.path}")
|
||||
)
|
||||
@@ -0,0 +1,201 @@
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.servers.basehttp import WSGIServer, get_internal_wsgi_application, run
|
||||
from django.db import connections
|
||||
from django.utils import autoreload
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.version import get_docs_version
|
||||
|
||||
naiveip_re = _lazy_re_compile(
|
||||
r"""^(?:
|
||||
(?P<addr>
|
||||
(?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) | # IPv4 address
|
||||
(?P<ipv6>\[[a-fA-F0-9:]+\]) | # IPv6 address
|
||||
(?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN
|
||||
):)?(?P<port>\d+)$""",
|
||||
re.X,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Starts a lightweight web server for development."
|
||||
|
||||
stealth_options = ("shutdown_message",)
|
||||
suppressed_base_arguments = {"--verbosity", "--traceback"}
|
||||
|
||||
default_addr = "127.0.0.1"
|
||||
default_addr_ipv6 = "::1"
|
||||
default_port = "8000"
|
||||
protocol = "http"
|
||||
server_cls = WSGIServer
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"addrport", nargs="?", help="Optional port number, or ipaddr:port"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ipv6",
|
||||
"-6",
|
||||
action="store_true",
|
||||
dest="use_ipv6",
|
||||
help="Tells Django to use an IPv6 address.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nothreading",
|
||||
action="store_false",
|
||||
dest="use_threading",
|
||||
help="Tells Django to NOT use threading.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noreload",
|
||||
action="store_false",
|
||||
dest="use_reloader",
|
||||
help="Tells Django to NOT use the auto-reloader.",
|
||||
)
|
||||
|
||||
def execute(self, *args, **options):
|
||||
if options["no_color"]:
|
||||
# We rely on the environment because it's currently the only
|
||||
# way to reach WSGIRequestHandler. This seems an acceptable
|
||||
# compromise considering `runserver` runs indefinitely.
|
||||
os.environ["DJANGO_COLORS"] = "nocolor"
|
||||
super().execute(*args, **options)
|
||||
|
||||
def get_handler(self, *args, **options):
|
||||
"""Return the default WSGI handler for the runner."""
|
||||
return get_internal_wsgi_application()
|
||||
|
||||
def get_check_kwargs(self, options):
|
||||
"""Validation is called explicitly each time the server reloads."""
|
||||
return {"tags": set()}
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not settings.DEBUG and not settings.ALLOWED_HOSTS:
|
||||
raise CommandError("You must set settings.ALLOWED_HOSTS if DEBUG is False.")
|
||||
|
||||
self.use_ipv6 = options["use_ipv6"]
|
||||
if self.use_ipv6 and not socket.has_ipv6:
|
||||
raise CommandError("Your Python does not support IPv6.")
|
||||
self._raw_ipv6 = False
|
||||
if not options["addrport"]:
|
||||
self.addr = ""
|
||||
self.port = self.default_port
|
||||
else:
|
||||
m = re.match(naiveip_re, options["addrport"])
|
||||
if m is None:
|
||||
raise CommandError(
|
||||
'"%s" is not a valid port number '
|
||||
"or address:port pair." % options["addrport"]
|
||||
)
|
||||
self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
|
||||
if not self.port.isdigit():
|
||||
raise CommandError("%r is not a valid port number." % self.port)
|
||||
if self.addr:
|
||||
if _ipv6:
|
||||
self.addr = self.addr[1:-1]
|
||||
self.use_ipv6 = True
|
||||
self._raw_ipv6 = True
|
||||
elif self.use_ipv6 and not _fqdn:
|
||||
raise CommandError('"%s" is not a valid IPv6 address.' % self.addr)
|
||||
if not self.addr:
|
||||
self.addr = self.default_addr_ipv6 if self.use_ipv6 else self.default_addr
|
||||
self._raw_ipv6 = self.use_ipv6
|
||||
self.run(**options)
|
||||
|
||||
def run(self, **options):
|
||||
"""Run the server, using the autoreloader if needed."""
|
||||
use_reloader = options["use_reloader"]
|
||||
|
||||
if use_reloader:
|
||||
autoreload.run_with_reloader(self.inner_run, **options)
|
||||
else:
|
||||
self.inner_run(None, **options)
|
||||
|
||||
def inner_run(self, *args, **options):
|
||||
# If an exception was silenced in ManagementUtility.execute in order
|
||||
# to be raised in the child process, raise it now.
|
||||
autoreload.raise_last_exception()
|
||||
|
||||
threading = options["use_threading"]
|
||||
# 'shutdown_message' is a stealth option.
|
||||
shutdown_message = options.get("shutdown_message", "")
|
||||
|
||||
if not options["skip_checks"]:
|
||||
self.stdout.write("Performing system checks...\n\n")
|
||||
check_kwargs = super().get_check_kwargs(options)
|
||||
check_kwargs["display_num_errors"] = True
|
||||
self.check(**check_kwargs)
|
||||
# Need to check migrations here, so can't use the
|
||||
# requires_migrations_check attribute.
|
||||
self.check_migrations()
|
||||
# Close all connections opened during migration checking.
|
||||
for conn in connections.all(initialized_only=True):
|
||||
conn.close()
|
||||
|
||||
try:
|
||||
handler = self.get_handler(*args, **options)
|
||||
run(
|
||||
self.addr,
|
||||
int(self.port),
|
||||
handler,
|
||||
ipv6=self.use_ipv6,
|
||||
threading=threading,
|
||||
on_bind=self.on_bind,
|
||||
server_cls=self.server_cls,
|
||||
)
|
||||
except OSError as e:
|
||||
# Use helpful error messages instead of ugly tracebacks.
|
||||
ERRORS = {
|
||||
errno.EACCES: "You don't have permission to access that port.",
|
||||
errno.EADDRINUSE: "That port is already in use.",
|
||||
errno.EADDRNOTAVAIL: "That IP address can't be assigned to.",
|
||||
}
|
||||
try:
|
||||
error_text = ERRORS[e.errno]
|
||||
except KeyError:
|
||||
error_text = e
|
||||
self.stderr.write("Error: %s" % error_text)
|
||||
# Need to use an OS exit because sys.exit doesn't work in a thread
|
||||
os._exit(1)
|
||||
except KeyboardInterrupt:
|
||||
if shutdown_message:
|
||||
self.stdout.write(shutdown_message)
|
||||
sys.exit(0)
|
||||
|
||||
def on_bind(self, server_port):
|
||||
quit_command = "CTRL-BREAK" if sys.platform == "win32" else "CONTROL-C"
|
||||
|
||||
if self._raw_ipv6:
|
||||
addr = f"[{self.addr}]"
|
||||
elif self.addr == "0":
|
||||
addr = "0.0.0.0"
|
||||
else:
|
||||
addr = self.addr
|
||||
|
||||
now = datetime.now().strftime("%B %d, %Y - %X")
|
||||
version = self.get_version()
|
||||
print(
|
||||
f"{now}\n"
|
||||
f"Django version {version}, using settings {settings.SETTINGS_MODULE!r}\n"
|
||||
f"Starting development server at {self.protocol}://{addr}:{server_port}/\n"
|
||||
f"Quit the server with {quit_command}.",
|
||||
file=self.stdout,
|
||||
)
|
||||
docs_version = get_docs_version()
|
||||
if os.environ.get("DJANGO_RUNSERVER_HIDE_WARNING") != "true":
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"WARNING: This is a development server. Do not use it in a "
|
||||
"production setting. Use a production WSGI or ASGI server "
|
||||
"instead.\nFor more information on production servers see: "
|
||||
f"https://docs.djangoproject.com/en/{docs_version}/howto/"
|
||||
"deployment/"
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
import socket
|
||||
|
||||
from django.core.mail import mail_admins, mail_managers, send_mail
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Sends a test email to the email addresses specified as arguments."
|
||||
missing_args_message = (
|
||||
"You must specify some email recipients, or pass the --managers or --admin "
|
||||
"options."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"email",
|
||||
nargs="*",
|
||||
help="One or more email addresses to send a test email to.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--managers",
|
||||
action="store_true",
|
||||
help="Send a test email to the addresses specified in settings.MANAGERS.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--admins",
|
||||
action="store_true",
|
||||
help="Send a test email to the addresses specified in settings.ADMINS.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
subject = "Test email from %s on %s" % (socket.gethostname(), timezone.now())
|
||||
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message="If you're reading this, it was successful.",
|
||||
from_email=None,
|
||||
recipient_list=kwargs["email"],
|
||||
)
|
||||
|
||||
if kwargs["managers"]:
|
||||
mail_managers(subject, "This email was sent to the site managers.")
|
||||
|
||||
if kwargs["admins"]:
|
||||
mail_admins(subject, "This email was sent to the site admins.")
|
||||
269
.venv/Lib/site-packages/django/core/management/commands/shell.py
Normal file
269
.venv/Lib/site-packages/django/core/management/commands/shell.py
Normal file
@@ -0,0 +1,269 @@
|
||||
import os
|
||||
import select
|
||||
import sys
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from django.utils.datastructures import OrderedSet
|
||||
from django.utils.module_loading import import_string as import_dotted_path
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Runs a Python interactive interpreter. Tries to use IPython or "
|
||||
"bpython, if one of them is available. Any standard input is executed "
|
||||
"as code."
|
||||
)
|
||||
|
||||
requires_system_checks = []
|
||||
shells = ["ipython", "bpython", "python"]
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--no-startup",
|
||||
action="store_true",
|
||||
help=(
|
||||
"When using plain Python, ignore the PYTHONSTARTUP environment "
|
||||
"variable and ~/.pythonrc.py script."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-imports",
|
||||
action="store_true",
|
||||
help="Disable automatic imports of models.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--interface",
|
||||
choices=self.shells,
|
||||
help=(
|
||||
"Specify an interactive interpreter interface. Available options: "
|
||||
'"ipython", "bpython", and "python"'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--command",
|
||||
help=(
|
||||
"Instead of opening an interactive shell, run a command as Django and "
|
||||
"exit."
|
||||
),
|
||||
)
|
||||
|
||||
def ipython(self, options):
|
||||
from IPython import start_ipython
|
||||
|
||||
start_ipython(argv=[], user_ns=self.get_namespace(**options))
|
||||
|
||||
def bpython(self, options):
|
||||
import bpython
|
||||
|
||||
bpython.embed(self.get_namespace(**options))
|
||||
|
||||
def python(self, options):
|
||||
import code
|
||||
|
||||
# Set up a dictionary to serve as the environment for the shell.
|
||||
imported_objects = self.get_namespace(**options)
|
||||
|
||||
# We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system
|
||||
# conventions and get $PYTHONSTARTUP first then .pythonrc.py.
|
||||
if not options["no_startup"]:
|
||||
for pythonrc in OrderedSet(
|
||||
[os.environ.get("PYTHONSTARTUP"), os.path.expanduser("~/.pythonrc.py")]
|
||||
):
|
||||
if not pythonrc:
|
||||
continue
|
||||
if not os.path.isfile(pythonrc):
|
||||
continue
|
||||
with open(pythonrc) as handle:
|
||||
pythonrc_code = handle.read()
|
||||
# Match the behavior of the cpython shell where an error in
|
||||
# PYTHONSTARTUP prints an exception and continues.
|
||||
try:
|
||||
exec(compile(pythonrc_code, pythonrc, "exec"), imported_objects)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
# By default, this will set up readline to do tab completion and to read and
|
||||
# write history to the .python_history file, but this can be overridden by
|
||||
# $PYTHONSTARTUP or ~/.pythonrc.py.
|
||||
try:
|
||||
hook = sys.__interactivehook__
|
||||
except AttributeError:
|
||||
# Match the behavior of the cpython shell where a missing
|
||||
# sys.__interactivehook__ is ignored.
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
hook()
|
||||
except Exception:
|
||||
# Match the behavior of the cpython shell where an error in
|
||||
# sys.__interactivehook__ prints a warning and the exception
|
||||
# and continues.
|
||||
print("Failed calling sys.__interactivehook__")
|
||||
traceback.print_exc()
|
||||
|
||||
# Set up tab completion for objects imported by $PYTHONSTARTUP or
|
||||
# ~/.pythonrc.py.
|
||||
try:
|
||||
import readline
|
||||
import rlcompleter
|
||||
|
||||
readline.set_completer(rlcompleter.Completer(imported_objects).complete)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Start the interactive interpreter.
|
||||
code.interact(local=imported_objects)
|
||||
|
||||
def get_auto_imports(self):
|
||||
"""Return a sequence of import paths for objects to be auto-imported.
|
||||
|
||||
By default, import paths for models in INSTALLED_APPS are included,
|
||||
with models from earlier apps taking precedence in case of a name
|
||||
collision.
|
||||
|
||||
For example, for an unchanged INSTALLED_APPS, this method returns:
|
||||
|
||||
[
|
||||
"django.contrib.sessions.models.Session",
|
||||
"django.contrib.contenttypes.models.ContentType",
|
||||
"django.contrib.auth.models.User",
|
||||
"django.contrib.auth.models.Group",
|
||||
"django.contrib.auth.models.Permission",
|
||||
"django.contrib.admin.models.LogEntry",
|
||||
]
|
||||
|
||||
"""
|
||||
app_models_imports = [
|
||||
f"{model.__module__}.{model.__name__}"
|
||||
for model in reversed(apps.get_models())
|
||||
if model.__module__
|
||||
]
|
||||
return app_models_imports
|
||||
|
||||
def get_namespace(self, **options):
|
||||
if options and options.get("no_imports"):
|
||||
return {}
|
||||
|
||||
verbosity = options["verbosity"] if options else 0
|
||||
|
||||
try:
|
||||
apps.check_models_ready()
|
||||
except AppRegistryNotReady:
|
||||
if verbosity > 0:
|
||||
settings_env_var = os.getenv("DJANGO_SETTINGS_MODULE")
|
||||
self.stdout.write(
|
||||
"Automatic imports are disabled since settings are not configured."
|
||||
f"\nDJANGO_SETTINGS_MODULE value is {settings_env_var!r}.\n"
|
||||
"HINT: Ensure that the settings module is configured and set.",
|
||||
self.style.ERROR,
|
||||
ending="\n\n",
|
||||
)
|
||||
return {}
|
||||
|
||||
path_imports = self.get_auto_imports()
|
||||
if path_imports is None:
|
||||
return {}
|
||||
|
||||
auto_imports = defaultdict(list)
|
||||
import_errors = []
|
||||
for path in path_imports:
|
||||
try:
|
||||
obj = import_dotted_path(path) if "." in path else import_module(path)
|
||||
except ImportError:
|
||||
import_errors.append(path)
|
||||
continue
|
||||
|
||||
if "." in path:
|
||||
module, name = path.rsplit(".", 1)
|
||||
else:
|
||||
module = None
|
||||
name = path
|
||||
if (name, obj) not in auto_imports[module]:
|
||||
auto_imports[module].append((name, obj))
|
||||
|
||||
namespace = {
|
||||
name: obj for items in auto_imports.values() for name, obj in items
|
||||
}
|
||||
|
||||
if verbosity < 1:
|
||||
return namespace
|
||||
|
||||
errors = len(import_errors)
|
||||
if errors:
|
||||
msg = "\n".join(f" {e}" for e in import_errors)
|
||||
objects = "objects" if errors != 1 else "object"
|
||||
self.stdout.write(
|
||||
f"{errors} {objects} could not be automatically imported:\n\n{msg}",
|
||||
self.style.ERROR,
|
||||
ending="\n\n",
|
||||
)
|
||||
|
||||
amount = len(namespace)
|
||||
objects_str = "objects" if amount != 1 else "object"
|
||||
msg = f"{amount} {objects_str} imported automatically"
|
||||
|
||||
if verbosity < 2:
|
||||
if amount:
|
||||
msg += " (use -v 2 for details)"
|
||||
self.stdout.write(f"{msg}.", self.style.SUCCESS, ending="\n\n")
|
||||
return namespace
|
||||
|
||||
top_level = auto_imports.pop(None, [])
|
||||
import_string = "\n".join(
|
||||
[f" import {obj}" for obj, _ in top_level]
|
||||
+ [
|
||||
f" from {module} import {objects}"
|
||||
for module, imported_objects in auto_imports.items()
|
||||
if (objects := ", ".join(i[0] for i in imported_objects))
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
import isort
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
import_string = isort.code(import_string)
|
||||
|
||||
if import_string:
|
||||
msg = f"{msg}:\n\n{import_string}"
|
||||
else:
|
||||
msg = f"{msg}."
|
||||
|
||||
self.stdout.write(msg, self.style.SUCCESS, ending="\n\n")
|
||||
|
||||
return namespace
|
||||
|
||||
def handle(self, **options):
|
||||
# Execute the command and exit.
|
||||
if options["command"]:
|
||||
exec(options["command"], {**globals(), **self.get_namespace(**options)})
|
||||
return
|
||||
|
||||
# Execute stdin if it has anything to read and exit.
|
||||
# Not supported on Windows due to select.select() limitations.
|
||||
if (
|
||||
sys.platform != "win32"
|
||||
and not sys.stdin.isatty()
|
||||
and select.select([sys.stdin], [], [], 0)[0]
|
||||
):
|
||||
exec(sys.stdin.read(), {**globals(), **self.get_namespace(**options)})
|
||||
return
|
||||
|
||||
available_shells = (
|
||||
[options["interface"]] if options["interface"] else self.shells
|
||||
)
|
||||
|
||||
for shell in available_shells:
|
||||
try:
|
||||
return getattr(self, shell)(options)
|
||||
except ImportError:
|
||||
pass
|
||||
raise CommandError("Couldn't import {} interface.".format(shell))
|
||||
@@ -0,0 +1,177 @@
|
||||
import sys
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.db.migrations.loader import MigrationLoader
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Shows all available migrations for the current project"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"app_label",
|
||||
nargs="*",
|
||||
help="App labels of applications to limit the output to.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help=(
|
||||
"Nominates a database to show migrations for. Defaults to the "
|
||||
'"default" database.'
|
||||
),
|
||||
)
|
||||
|
||||
formats = parser.add_mutually_exclusive_group()
|
||||
formats.add_argument(
|
||||
"--list",
|
||||
"-l",
|
||||
action="store_const",
|
||||
dest="format",
|
||||
const="list",
|
||||
help=(
|
||||
"Shows a list of all migrations and which are applied. "
|
||||
"With a verbosity level of 2 or above, the applied datetimes "
|
||||
"will be included."
|
||||
),
|
||||
)
|
||||
formats.add_argument(
|
||||
"--plan",
|
||||
"-p",
|
||||
action="store_const",
|
||||
dest="format",
|
||||
const="plan",
|
||||
help=(
|
||||
"Shows all migrations in the order they will be applied. With a "
|
||||
"verbosity level of 2 or above all direct migration dependencies and "
|
||||
"reverse dependencies (run_before) will be included."
|
||||
),
|
||||
)
|
||||
|
||||
parser.set_defaults(format="list")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.verbosity = options["verbosity"]
|
||||
|
||||
# Get the database we're operating from
|
||||
db = options["database"]
|
||||
connection = connections[db]
|
||||
|
||||
if options["format"] == "plan":
|
||||
return self.show_plan(connection, options["app_label"])
|
||||
else:
|
||||
return self.show_list(connection, options["app_label"])
|
||||
|
||||
def _validate_app_names(self, loader, app_names):
|
||||
has_bad_names = False
|
||||
for app_name in app_names:
|
||||
try:
|
||||
apps.get_app_config(app_name)
|
||||
except LookupError as err:
|
||||
self.stderr.write(str(err))
|
||||
has_bad_names = True
|
||||
if has_bad_names:
|
||||
sys.exit(2)
|
||||
|
||||
def show_list(self, connection, app_names=None):
|
||||
"""
|
||||
Show a list of all migrations on the system, or only those of
|
||||
some named apps.
|
||||
"""
|
||||
# Load migrations from disk/DB
|
||||
loader = MigrationLoader(connection, ignore_no_migrations=True)
|
||||
recorder = MigrationRecorder(connection)
|
||||
recorded_migrations = recorder.applied_migrations()
|
||||
graph = loader.graph
|
||||
# If we were passed a list of apps, validate it
|
||||
if app_names:
|
||||
self._validate_app_names(loader, app_names)
|
||||
# Otherwise, show all apps in alphabetic order
|
||||
else:
|
||||
app_names = sorted(loader.migrated_apps)
|
||||
# For each app, print its migrations in order from oldest (roots) to
|
||||
# newest (leaves).
|
||||
for app_name in app_names:
|
||||
self.stdout.write(app_name, self.style.MIGRATE_LABEL)
|
||||
shown = set()
|
||||
for node in graph.leaf_nodes(app_name):
|
||||
for plan_node in graph.forwards_plan(node):
|
||||
if plan_node not in shown and plan_node[0] == app_name:
|
||||
# Give it a nice title if it's a squashed one
|
||||
title = plan_node[1]
|
||||
if graph.nodes[plan_node].replaces:
|
||||
title += " (%s squashed migrations)" % len(
|
||||
graph.nodes[plan_node].replaces
|
||||
)
|
||||
applied_migration = loader.applied_migrations.get(plan_node)
|
||||
# Mark it as applied/unapplied
|
||||
if applied_migration:
|
||||
if plan_node in recorded_migrations:
|
||||
output = " [X] %s" % title
|
||||
else:
|
||||
title += " Run 'manage.py migrate' to finish recording."
|
||||
output = " [-] %s" % title
|
||||
if self.verbosity >= 2 and hasattr(
|
||||
applied_migration, "applied"
|
||||
):
|
||||
output += (
|
||||
" (applied at %s)"
|
||||
% applied_migration.applied.strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
)
|
||||
self.stdout.write(output)
|
||||
else:
|
||||
self.stdout.write(" [ ] %s" % title)
|
||||
shown.add(plan_node)
|
||||
# If we didn't print anything, then a small message
|
||||
if not shown:
|
||||
self.stdout.write(" (no migrations)", self.style.ERROR)
|
||||
|
||||
def show_plan(self, connection, app_names=None):
|
||||
"""
|
||||
Show all known migrations (or only those of the specified app_names)
|
||||
in the order they will be applied.
|
||||
"""
|
||||
# Load migrations from disk/DB
|
||||
loader = MigrationLoader(connection)
|
||||
graph = loader.graph
|
||||
if app_names:
|
||||
self._validate_app_names(loader, app_names)
|
||||
targets = [key for key in graph.leaf_nodes() if key[0] in app_names]
|
||||
else:
|
||||
targets = graph.leaf_nodes()
|
||||
plan = []
|
||||
seen = set()
|
||||
|
||||
# Generate the plan
|
||||
for target in targets:
|
||||
for migration in graph.forwards_plan(target):
|
||||
if migration not in seen:
|
||||
node = graph.node_map[migration]
|
||||
plan.append(node)
|
||||
seen.add(migration)
|
||||
|
||||
# Output
|
||||
def print_deps(node):
|
||||
out = []
|
||||
for parent in sorted(node.parents):
|
||||
out.append("%s.%s" % parent.key)
|
||||
if out:
|
||||
return " ... (%s)" % ", ".join(out)
|
||||
return ""
|
||||
|
||||
for node in plan:
|
||||
deps = ""
|
||||
if self.verbosity >= 2:
|
||||
deps = print_deps(node)
|
||||
if node.key in loader.applied_migrations:
|
||||
self.stdout.write("[X] %s.%s%s" % (node.key[0], node.key[1], deps))
|
||||
else:
|
||||
self.stdout.write("[ ] %s.%s%s" % (node.key[0], node.key[1], deps))
|
||||
if not plan:
|
||||
self.stdout.write("(no migrations)", self.style.ERROR)
|
||||
@@ -0,0 +1,30 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.sql import sql_flush
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Returns a list of the SQL statements required to return all tables in "
|
||||
"the database to the state they were in just after they were installed."
|
||||
)
|
||||
|
||||
output_transaction = True
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help=(
|
||||
'Nominates a database to print the SQL for. Defaults to the "default" '
|
||||
"database."
|
||||
),
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
sql_statements = sql_flush(self.style, connections[options["database"]])
|
||||
if not sql_statements and options["verbosity"] >= 1:
|
||||
self.stderr.write("No tables found.")
|
||||
return "\n".join(sql_statements)
|
||||
@@ -0,0 +1,83 @@
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.db.migrations.loader import AmbiguityError, MigrationLoader
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Prints the SQL statements for the named migration."
|
||||
|
||||
output_transaction = True
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"app_label", help="App label of the application containing the migration."
|
||||
)
|
||||
parser.add_argument(
|
||||
"migration_name", help="Migration name to print the SQL for."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help=(
|
||||
'Nominates a database to create SQL for. Defaults to the "default" '
|
||||
"database."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backwards",
|
||||
action="store_true",
|
||||
help="Creates SQL to unapply the migration, rather than to apply it",
|
||||
)
|
||||
|
||||
def execute(self, *args, **options):
|
||||
# sqlmigrate doesn't support coloring its output, so make the
|
||||
# BEGIN/COMMIT statements added by output_transaction colorless also.
|
||||
self.style.SQL_KEYWORD = lambda noop: noop
|
||||
return super().execute(*args, **options)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Get the database we're operating from
|
||||
connection = connections[options["database"]]
|
||||
|
||||
# Load up a loader to get all the migration data, but don't replace
|
||||
# migrations.
|
||||
loader = MigrationLoader(connection, replace_migrations=False)
|
||||
|
||||
# Resolve command-line arguments into a migration
|
||||
app_label, migration_name = options["app_label"], options["migration_name"]
|
||||
# Validate app_label
|
||||
try:
|
||||
apps.get_app_config(app_label)
|
||||
except LookupError as err:
|
||||
raise CommandError(str(err))
|
||||
if app_label not in loader.migrated_apps:
|
||||
raise CommandError("App '%s' does not have migrations" % app_label)
|
||||
try:
|
||||
migration = loader.get_migration_by_prefix(app_label, migration_name)
|
||||
except AmbiguityError:
|
||||
raise CommandError(
|
||||
"More than one migration matches '%s' in app '%s'. Please be more "
|
||||
"specific." % (migration_name, app_label)
|
||||
)
|
||||
except KeyError:
|
||||
raise CommandError(
|
||||
"Cannot find a migration matching '%s' from app '%s'. Is it in "
|
||||
"INSTALLED_APPS?" % (migration_name, app_label)
|
||||
)
|
||||
target = (app_label, migration.name)
|
||||
|
||||
# Show begin/end around output for atomic migrations, if the database
|
||||
# supports transactional DDL.
|
||||
self.output_transaction = (
|
||||
migration.atomic and connection.features.can_rollback_ddl
|
||||
)
|
||||
|
||||
# Make a plan that represents just the requested migrations and show SQL
|
||||
# for it
|
||||
plan = [(loader.graph.nodes[target], options["backwards"])]
|
||||
sql_statements = loader.collect_sql(plan)
|
||||
if not sql_statements and options["verbosity"] >= 1:
|
||||
self.stderr.write("No operations found.")
|
||||
return "\n".join(sql_statements)
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.core.management.base import AppCommand
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
|
||||
|
||||
class Command(AppCommand):
|
||||
help = (
|
||||
"Prints the SQL statements for resetting sequences for the given app name(s)."
|
||||
)
|
||||
|
||||
output_transaction = True
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
choices=tuple(connections),
|
||||
help=(
|
||||
'Nominates a database to print the SQL for. Defaults to the "default" '
|
||||
"database."
|
||||
),
|
||||
)
|
||||
|
||||
def handle_app_config(self, app_config, **options):
|
||||
if app_config.models_module is None:
|
||||
return
|
||||
connection = connections[options["database"]]
|
||||
models = app_config.get_models(include_auto_created=True)
|
||||
statements = connection.ops.sequence_reset_sql(self.style, models)
|
||||
if not statements and options["verbosity"] >= 1:
|
||||
self.stderr.write("No sequences found.")
|
||||
return "\n".join(statements)
|
||||
@@ -0,0 +1,267 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.utils import run_formatters
|
||||
from django.db import DEFAULT_DB_ALIAS, connections, migrations
|
||||
from django.db.migrations.loader import AmbiguityError, MigrationLoader
|
||||
from django.db.migrations.migration import SwappableTuple
|
||||
from django.db.migrations.optimizer import MigrationOptimizer
|
||||
from django.db.migrations.writer import MigrationWriter
|
||||
from django.utils.version import get_docs_version
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Squashes an existing set of migrations (from first until specified) into a "
|
||||
"single new one."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"app_label",
|
||||
help="App label of the application to squash migrations for.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"start_migration_name",
|
||||
nargs="?",
|
||||
help=(
|
||||
"Migrations will be squashed starting from and including this "
|
||||
"migration."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"migration_name",
|
||||
help="Migrations will be squashed until and including this migration.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-optimize",
|
||||
action="store_true",
|
||||
help="Do not try to optimize the squashed operations.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
help="Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--squashed-name",
|
||||
help="Sets the name of the new squashed migration.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-header",
|
||||
action="store_false",
|
||||
dest="include_header",
|
||||
help="Do not add a header comment to the new squashed migration.",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
self.verbosity = options["verbosity"]
|
||||
self.interactive = options["interactive"]
|
||||
app_label = options["app_label"]
|
||||
start_migration_name = options["start_migration_name"]
|
||||
migration_name = options["migration_name"]
|
||||
no_optimize = options["no_optimize"]
|
||||
squashed_name = options["squashed_name"]
|
||||
include_header = options["include_header"]
|
||||
# Validate app_label.
|
||||
try:
|
||||
apps.get_app_config(app_label)
|
||||
except LookupError as err:
|
||||
raise CommandError(str(err))
|
||||
# Load the current graph state, check the app and migration they asked
|
||||
# for exists.
|
||||
loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
|
||||
if app_label not in loader.migrated_apps:
|
||||
raise CommandError(
|
||||
"App '%s' does not have migrations (so squashmigrations on "
|
||||
"it makes no sense)" % app_label
|
||||
)
|
||||
|
||||
migration = self.find_migration(loader, app_label, migration_name)
|
||||
|
||||
# Work out the list of predecessor migrations
|
||||
migrations_to_squash = [
|
||||
loader.get_migration(al, mn)
|
||||
for al, mn in loader.graph.forwards_plan(
|
||||
(migration.app_label, migration.name)
|
||||
)
|
||||
if al == migration.app_label
|
||||
]
|
||||
|
||||
if start_migration_name:
|
||||
start_migration = self.find_migration(
|
||||
loader, app_label, start_migration_name
|
||||
)
|
||||
start = loader.get_migration(
|
||||
start_migration.app_label, start_migration.name
|
||||
)
|
||||
try:
|
||||
start_index = migrations_to_squash.index(start)
|
||||
migrations_to_squash = migrations_to_squash[start_index:]
|
||||
except ValueError:
|
||||
raise CommandError(
|
||||
"The migration '%s' cannot be found. Maybe it comes after "
|
||||
"the migration '%s'?\n"
|
||||
"Have a look at:\n"
|
||||
" python manage.py showmigrations %s\n"
|
||||
"to debug this issue." % (start_migration, migration, app_label)
|
||||
)
|
||||
|
||||
# Tell them what we're doing and optionally ask if we should proceed
|
||||
if self.verbosity > 0 or self.interactive:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING("Will squash the following migrations:")
|
||||
)
|
||||
for migration in migrations_to_squash:
|
||||
self.stdout.write(" - %s" % migration.name)
|
||||
|
||||
if self.interactive:
|
||||
answer = None
|
||||
while not answer or answer not in "yn":
|
||||
answer = input("Do you wish to proceed? [y/N] ")
|
||||
if not answer:
|
||||
answer = "n"
|
||||
break
|
||||
else:
|
||||
answer = answer[0].lower()
|
||||
if answer != "y":
|
||||
return
|
||||
|
||||
# Load the operations from all those migrations and concat together,
|
||||
# along with collecting external dependencies and detecting
|
||||
# double-squashing
|
||||
operations = []
|
||||
dependencies = set()
|
||||
# We need to take all dependencies from the first migration in the list
|
||||
# as it may be 0002 depending on 0001
|
||||
first_migration = True
|
||||
for smigration in migrations_to_squash:
|
||||
if smigration.replaces:
|
||||
raise CommandError(
|
||||
"You cannot squash squashed migrations! Please transition it to a "
|
||||
"normal migration first: https://docs.djangoproject.com/en/%s/"
|
||||
"topics/migrations/#squashing-migrations" % get_docs_version()
|
||||
)
|
||||
operations.extend(smigration.operations)
|
||||
for dependency in smigration.dependencies:
|
||||
if isinstance(dependency, SwappableTuple):
|
||||
if settings.AUTH_USER_MODEL == dependency.setting:
|
||||
dependencies.add(("__setting__", "AUTH_USER_MODEL"))
|
||||
else:
|
||||
dependencies.add(dependency)
|
||||
elif dependency[0] != smigration.app_label or first_migration:
|
||||
dependencies.add(dependency)
|
||||
first_migration = False
|
||||
|
||||
if no_optimize:
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING("(Skipping optimization.)")
|
||||
)
|
||||
new_operations = operations
|
||||
else:
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write(self.style.MIGRATE_HEADING("Optimizing..."))
|
||||
|
||||
optimizer = MigrationOptimizer()
|
||||
new_operations = optimizer.optimize(operations, migration.app_label)
|
||||
|
||||
if self.verbosity > 0:
|
||||
if len(new_operations) == len(operations):
|
||||
self.stdout.write(" No optimizations possible.")
|
||||
else:
|
||||
self.stdout.write(
|
||||
" Optimized from %s operations to %s operations."
|
||||
% (len(operations), len(new_operations))
|
||||
)
|
||||
|
||||
# Work out the value of replaces (any squashed ones we're re-squashing)
|
||||
# need to feed their replaces into ours
|
||||
replaces = []
|
||||
for migration in migrations_to_squash:
|
||||
if migration.replaces:
|
||||
replaces.extend(migration.replaces)
|
||||
else:
|
||||
replaces.append((migration.app_label, migration.name))
|
||||
|
||||
# Make a new migration with those operations
|
||||
subclass = type(
|
||||
"Migration",
|
||||
(migrations.Migration,),
|
||||
{
|
||||
"dependencies": dependencies,
|
||||
"operations": new_operations,
|
||||
"replaces": replaces,
|
||||
},
|
||||
)
|
||||
if start_migration_name:
|
||||
if squashed_name:
|
||||
# Use the name from --squashed-name.
|
||||
prefix, _ = start_migration.name.split("_", 1)
|
||||
name = "%s_%s" % (prefix, squashed_name)
|
||||
else:
|
||||
# Generate a name.
|
||||
name = "%s_squashed_%s" % (start_migration.name, migration.name)
|
||||
new_migration = subclass(name, app_label)
|
||||
else:
|
||||
name = "0001_%s" % (squashed_name or "squashed_%s" % migration.name)
|
||||
new_migration = subclass(name, app_label)
|
||||
new_migration.initial = True
|
||||
|
||||
# Write out the new migration file
|
||||
writer = MigrationWriter(new_migration, include_header)
|
||||
if os.path.exists(writer.path):
|
||||
raise CommandError(
|
||||
f"Migration {new_migration.name} already exists. Use a different name."
|
||||
)
|
||||
with open(writer.path, "w", encoding="utf-8") as fh:
|
||||
fh.write(writer.as_string())
|
||||
run_formatters([writer.path], stderr=self.stderr)
|
||||
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING(
|
||||
"Created new squashed migration %s" % writer.path
|
||||
)
|
||||
+ "\n"
|
||||
" You should commit this migration but leave the old ones in place;\n"
|
||||
" the new migration will be used for new installs. Once you are sure\n"
|
||||
" all instances of the codebase have applied the migrations you "
|
||||
"squashed,\n"
|
||||
" you can delete them."
|
||||
)
|
||||
if writer.needs_manual_porting:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING("Manual porting required") + "\n"
|
||||
" Your migrations contained functions that must be manually "
|
||||
"copied over,\n"
|
||||
" as we could not safely copy their implementation.\n"
|
||||
" See the comment at the top of the squashed migration for "
|
||||
"details."
|
||||
)
|
||||
if shutil.which("black"):
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
"Squashed migration couldn't be formatted using the "
|
||||
'"black" command. You can call it manually.'
|
||||
)
|
||||
)
|
||||
|
||||
def find_migration(self, loader, app_label, name):
|
||||
try:
|
||||
return loader.get_migration_by_prefix(app_label, name)
|
||||
except AmbiguityError:
|
||||
raise CommandError(
|
||||
"More than one migration matches '%s' in app '%s'. Please be "
|
||||
"more specific." % (name, app_label)
|
||||
)
|
||||
except KeyError:
|
||||
raise CommandError(
|
||||
"Cannot find a migration matching '%s' from app '%s'."
|
||||
% (name, app_label)
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.core.management.templates import TemplateCommand
|
||||
|
||||
|
||||
class Command(TemplateCommand):
|
||||
help = (
|
||||
"Creates a Django app directory structure for the given app name in "
|
||||
"the current directory or optionally in the given directory."
|
||||
)
|
||||
missing_args_message = "You must provide an application name."
|
||||
|
||||
def handle(self, **options):
|
||||
app_name = options.pop("name")
|
||||
target = options.pop("directory")
|
||||
super().handle("app", app_name, target, **options)
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.core.checks.security.base import SECRET_KEY_INSECURE_PREFIX
|
||||
from django.core.management.templates import TemplateCommand
|
||||
|
||||
from ..utils import get_random_secret_key
|
||||
|
||||
|
||||
class Command(TemplateCommand):
|
||||
help = (
|
||||
"Creates a Django project directory structure for the given project "
|
||||
"name in the current directory or optionally in the given directory."
|
||||
)
|
||||
missing_args_message = "You must provide a project name."
|
||||
|
||||
def handle(self, **options):
|
||||
project_name = options.pop("name")
|
||||
target = options.pop("directory")
|
||||
|
||||
# Create a random SECRET_KEY to put it in the main settings.
|
||||
options["secret_key"] = SECRET_KEY_INSECURE_PREFIX + get_random_secret_key()
|
||||
|
||||
super().handle("project", project_name, target, **options)
|
||||
@@ -0,0 +1,66 @@
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.utils import get_command_line_option
|
||||
from django.test.runner import get_max_test_processes
|
||||
from django.test.utils import NullTimeKeeper, TimeKeeper, get_runner
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Discover and run tests in the specified modules or the current directory."
|
||||
|
||||
# DiscoverRunner runs the checks after databases are set up.
|
||||
requires_system_checks = []
|
||||
test_runner = None
|
||||
|
||||
def run_from_argv(self, argv):
|
||||
"""
|
||||
Pre-parse the command line to extract the value of the --testrunner
|
||||
option. This allows a test runner to define additional command line
|
||||
arguments.
|
||||
"""
|
||||
self.test_runner = get_command_line_option(argv, "--testrunner")
|
||||
super().run_from_argv(argv)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"args",
|
||||
metavar="test_label",
|
||||
nargs="*",
|
||||
help=(
|
||||
"Module paths to test; can be modulename, modulename.TestCase or "
|
||||
"modulename.TestCase.test_method"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
help="Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--testrunner",
|
||||
help="Tells Django to use specified test runner class instead of "
|
||||
"the one specified by the TEST_RUNNER setting.",
|
||||
)
|
||||
|
||||
test_runner_class = get_runner(settings, self.test_runner)
|
||||
|
||||
if hasattr(test_runner_class, "add_arguments"):
|
||||
test_runner_class.add_arguments(parser)
|
||||
|
||||
def handle(self, *test_labels, **options):
|
||||
TestRunner = get_runner(settings, options["testrunner"])
|
||||
|
||||
time_keeper = TimeKeeper() if options.get("timing", False) else NullTimeKeeper()
|
||||
parallel = options.get("parallel")
|
||||
if parallel == "auto":
|
||||
options["parallel"] = get_max_test_processes()
|
||||
test_runner = TestRunner(**options)
|
||||
with time_keeper.timed("Total run"):
|
||||
failures = test_runner.run_tests(test_labels)
|
||||
time_keeper.print_results()
|
||||
if failures:
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,65 @@
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import connection
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Runs a development server with data from the given fixture(s)."
|
||||
|
||||
requires_system_checks = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"args",
|
||||
metavar="fixture",
|
||||
nargs="*",
|
||||
help="Path(s) to fixtures to load before running the server.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
help="Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--addrport",
|
||||
default="",
|
||||
help="Port number or ipaddr:port to run the server on.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ipv6",
|
||||
"-6",
|
||||
action="store_true",
|
||||
dest="use_ipv6",
|
||||
help="Tells Django to use an IPv6 address.",
|
||||
)
|
||||
|
||||
def handle(self, *fixture_labels, **options):
|
||||
verbosity = options["verbosity"]
|
||||
interactive = options["interactive"]
|
||||
|
||||
# Create a test database.
|
||||
db_name = connection.creation.create_test_db(
|
||||
verbosity=verbosity, autoclobber=not interactive, serialize=False
|
||||
)
|
||||
|
||||
# Import the fixture data into the test database.
|
||||
call_command("loaddata", *fixture_labels, verbosity=verbosity)
|
||||
|
||||
# Run the development server. Turn off auto-reloading because it causes
|
||||
# a strange error -- it causes this handle() method to be called
|
||||
# multiple times.
|
||||
shutdown_message = (
|
||||
"\nServer stopped.\nNote that the test database, %r, has not been "
|
||||
"deleted. You can explore it on your own." % db_name
|
||||
)
|
||||
use_threading = connection.features.test_db_allows_multiple_connections
|
||||
call_command(
|
||||
"runserver",
|
||||
addrport=options["addrport"],
|
||||
shutdown_message=shutdown_message,
|
||||
use_reloader=False,
|
||||
use_ipv6=options["use_ipv6"],
|
||||
use_threading=use_threading,
|
||||
)
|
||||
59
.venv/Lib/site-packages/django/core/management/sql.py
Normal file
59
.venv/Lib/site-packages/django/core/management/sql.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import sys
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
|
||||
|
||||
def sql_flush(style, connection, reset_sequences=True, allow_cascade=False):
|
||||
"""
|
||||
Return a list of the SQL statements used to flush the database.
|
||||
"""
|
||||
tables = connection.introspection.django_table_names(
|
||||
only_existing=True, include_views=False
|
||||
)
|
||||
return connection.ops.sql_flush(
|
||||
style,
|
||||
tables,
|
||||
reset_sequences=reset_sequences,
|
||||
allow_cascade=allow_cascade,
|
||||
)
|
||||
|
||||
|
||||
def emit_pre_migrate_signal(verbosity, interactive, db, **kwargs):
|
||||
# Emit the pre_migrate signal for every application.
|
||||
for app_config in apps.get_app_configs():
|
||||
if app_config.models_module is None:
|
||||
continue
|
||||
if verbosity >= 2:
|
||||
stdout = kwargs.get("stdout", sys.stdout)
|
||||
stdout.write(
|
||||
"Running pre-migrate handlers for application %s" % app_config.label
|
||||
)
|
||||
models.signals.pre_migrate.send(
|
||||
sender=app_config,
|
||||
app_config=app_config,
|
||||
verbosity=verbosity,
|
||||
interactive=interactive,
|
||||
using=db,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def emit_post_migrate_signal(verbosity, interactive, db, **kwargs):
|
||||
# Emit the post_migrate signal for every application.
|
||||
for app_config in apps.get_app_configs():
|
||||
if app_config.models_module is None:
|
||||
continue
|
||||
if verbosity >= 2:
|
||||
stdout = kwargs.get("stdout", sys.stdout)
|
||||
stdout.write(
|
||||
"Running post-migrate handlers for application %s" % app_config.label
|
||||
)
|
||||
models.signals.post_migrate.send(
|
||||
sender=app_config,
|
||||
app_config=app_config,
|
||||
verbosity=verbosity,
|
||||
interactive=interactive,
|
||||
using=db,
|
||||
**kwargs,
|
||||
)
|
||||
402
.venv/Lib/site-packages/django/core/management/templates.py
Normal file
402
.venv/Lib/site-packages/django/core/management/templates.py
Normal file
@@ -0,0 +1,402 @@
|
||||
import argparse
|
||||
import mimetypes
|
||||
import os
|
||||
import posixpath
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
from importlib.util import find_spec
|
||||
from urllib.request import build_opener
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.utils import (
|
||||
find_formatters,
|
||||
handle_extensions,
|
||||
run_formatters,
|
||||
)
|
||||
from django.template import Context, Engine
|
||||
from django.utils import archive
|
||||
from django.utils.http import parse_header_parameters
|
||||
from django.utils.version import get_docs_version
|
||||
|
||||
|
||||
class TemplateCommand(BaseCommand):
|
||||
"""
|
||||
Copy either a Django application layout template or a Django project
|
||||
layout template into the specified directory.
|
||||
|
||||
:param style: A color style object (see django.core.management.color).
|
||||
:param app_or_project: The string 'app' or 'project'.
|
||||
:param name: The name of the application or project.
|
||||
:param directory: The directory to which the template should be copied.
|
||||
:param options: The additional variables passed to project or app templates
|
||||
"""
|
||||
|
||||
requires_system_checks = []
|
||||
# The supported URL schemes
|
||||
url_schemes = ["http", "https", "ftp"]
|
||||
# Rewrite the following suffixes when determining the target filename.
|
||||
rewrite_template_suffixes = (
|
||||
# Allow shipping invalid .py files without byte-compilation.
|
||||
(".py-tpl", ".py"),
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("name", help="Name of the application or project.")
|
||||
parser.add_argument(
|
||||
"directory", nargs="?", help="Optional destination directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--template", help="The path or URL to load the template from."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extension",
|
||||
"-e",
|
||||
dest="extensions",
|
||||
action="append",
|
||||
default=["py"],
|
||||
help='The file extension(s) to render (default: "py"). '
|
||||
"Separate multiple extensions with commas, or use "
|
||||
"-e multiple times.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
"-n",
|
||||
dest="files",
|
||||
action="append",
|
||||
default=[],
|
||||
help="The file name(s) to render. Separate multiple file names "
|
||||
"with commas, or use -n multiple times.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exclude",
|
||||
"-x",
|
||||
action="append",
|
||||
default=argparse.SUPPRESS,
|
||||
nargs="?",
|
||||
const="",
|
||||
help=(
|
||||
"The directory name(s) to exclude, in addition to .git and "
|
||||
"__pycache__. Can be used multiple times."
|
||||
),
|
||||
)
|
||||
|
||||
def handle(self, app_or_project, name, target=None, **options):
|
||||
self.app_or_project = app_or_project
|
||||
self.a_or_an = "an" if app_or_project == "app" else "a"
|
||||
self.paths_to_remove = []
|
||||
self.verbosity = options["verbosity"]
|
||||
|
||||
self.validate_name(name)
|
||||
|
||||
# if some directory is given, make sure it's nicely expanded
|
||||
if target is None:
|
||||
top_dir = os.path.join(os.getcwd(), name)
|
||||
try:
|
||||
os.makedirs(top_dir)
|
||||
except FileExistsError:
|
||||
raise CommandError("'%s' already exists" % top_dir)
|
||||
except OSError as e:
|
||||
raise CommandError(e)
|
||||
else:
|
||||
top_dir = os.path.abspath(os.path.expanduser(target))
|
||||
if app_or_project == "app":
|
||||
self.validate_name(os.path.basename(top_dir), "directory")
|
||||
if not os.path.exists(top_dir):
|
||||
raise CommandError(
|
||||
"Destination directory '%s' does not "
|
||||
"exist, please create it first." % top_dir
|
||||
)
|
||||
|
||||
# Find formatters, which are external executables, before input
|
||||
# from the templates can sneak into the path.
|
||||
formatter_paths = find_formatters()
|
||||
|
||||
extensions = tuple(handle_extensions(options["extensions"]))
|
||||
extra_files = []
|
||||
excluded_directories = [".git", "__pycache__"]
|
||||
for file in options["files"]:
|
||||
extra_files.extend(map(lambda x: x.strip(), file.split(",")))
|
||||
if exclude := options.get("exclude"):
|
||||
for directory in exclude:
|
||||
excluded_directories.append(directory.strip())
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write(
|
||||
"Rendering %s template files with extensions: %s"
|
||||
% (app_or_project, ", ".join(extensions))
|
||||
)
|
||||
self.stdout.write(
|
||||
"Rendering %s template files with filenames: %s"
|
||||
% (app_or_project, ", ".join(extra_files))
|
||||
)
|
||||
base_name = "%s_name" % app_or_project
|
||||
base_subdir = "%s_template" % app_or_project
|
||||
base_directory = "%s_directory" % app_or_project
|
||||
camel_case_name = "camel_case_%s_name" % app_or_project
|
||||
camel_case_value = "".join(x for x in name.title() if x != "_")
|
||||
|
||||
context = Context(
|
||||
{
|
||||
**options,
|
||||
base_name: name,
|
||||
base_directory: top_dir,
|
||||
camel_case_name: camel_case_value,
|
||||
"docs_version": get_docs_version(),
|
||||
"django_version": django.__version__,
|
||||
},
|
||||
autoescape=False,
|
||||
)
|
||||
|
||||
# Setup a stub settings environment for template rendering
|
||||
if not settings.configured:
|
||||
settings.configure()
|
||||
django.setup()
|
||||
|
||||
template_dir = self.handle_template(options["template"], base_subdir)
|
||||
prefix_length = len(template_dir) + 1
|
||||
|
||||
for root, dirs, files in os.walk(template_dir):
|
||||
path_rest = root[prefix_length:]
|
||||
relative_dir = path_rest.replace(base_name, name)
|
||||
if relative_dir:
|
||||
target_dir = os.path.join(top_dir, relative_dir)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
for dirname in dirs[:]:
|
||||
if "exclude" not in options:
|
||||
if dirname.startswith(".") or dirname == "__pycache__":
|
||||
dirs.remove(dirname)
|
||||
elif dirname in excluded_directories:
|
||||
dirs.remove(dirname)
|
||||
|
||||
for filename in files:
|
||||
if filename.endswith((".pyo", ".pyc", ".py.class")):
|
||||
# Ignore some files as they cause various breakages.
|
||||
continue
|
||||
old_path = os.path.join(root, filename)
|
||||
new_path = os.path.join(
|
||||
top_dir, relative_dir, filename.replace(base_name, name)
|
||||
)
|
||||
for old_suffix, new_suffix in self.rewrite_template_suffixes:
|
||||
if new_path.endswith(old_suffix):
|
||||
new_path = new_path.removesuffix(old_suffix) + new_suffix
|
||||
break # Only rewrite once
|
||||
|
||||
if os.path.exists(new_path):
|
||||
raise CommandError(
|
||||
"%s already exists. Overlaying %s %s into an existing "
|
||||
"directory won't replace conflicting files."
|
||||
% (
|
||||
new_path,
|
||||
self.a_or_an,
|
||||
app_or_project,
|
||||
)
|
||||
)
|
||||
|
||||
# Only render the Python files, as we don't want to
|
||||
# accidentally render Django templates files
|
||||
if new_path.endswith(extensions) or filename in extra_files:
|
||||
with open(old_path, encoding="utf-8") as template_file:
|
||||
content = template_file.read()
|
||||
template = Engine().from_string(content)
|
||||
content = template.render(context)
|
||||
with open(new_path, "w", encoding="utf-8") as new_file:
|
||||
new_file.write(content)
|
||||
else:
|
||||
shutil.copyfile(old_path, new_path)
|
||||
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Creating %s" % new_path)
|
||||
try:
|
||||
self.apply_umask(old_path, new_path)
|
||||
self.make_writeable(new_path)
|
||||
except OSError:
|
||||
self.stderr.write(
|
||||
"Notice: Couldn't set permission bits on %s. You're "
|
||||
"probably using an uncommon filesystem setup. No "
|
||||
"problem." % new_path,
|
||||
self.style.NOTICE,
|
||||
)
|
||||
|
||||
if self.paths_to_remove:
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Cleaning up temporary files.")
|
||||
for path_to_remove in self.paths_to_remove:
|
||||
if os.path.isfile(path_to_remove):
|
||||
os.remove(path_to_remove)
|
||||
else:
|
||||
shutil.rmtree(path_to_remove)
|
||||
|
||||
run_formatters([top_dir], **formatter_paths, stderr=self.stderr)
|
||||
|
||||
def handle_template(self, template, subdir):
|
||||
"""
|
||||
Determine where the app or project templates are.
|
||||
Use django.__path__[0] as the default because the Django install
|
||||
directory isn't known.
|
||||
"""
|
||||
if template is None:
|
||||
return os.path.join(django.__path__[0], "conf", subdir)
|
||||
else:
|
||||
template = template.removeprefix("file://")
|
||||
expanded_template = os.path.expanduser(template)
|
||||
expanded_template = os.path.normpath(expanded_template)
|
||||
if os.path.isdir(expanded_template):
|
||||
return expanded_template
|
||||
if self.is_url(template):
|
||||
# downloads the file and returns the path
|
||||
absolute_path = self.download(template)
|
||||
else:
|
||||
absolute_path = os.path.abspath(expanded_template)
|
||||
if os.path.exists(absolute_path):
|
||||
return self.extract(absolute_path)
|
||||
|
||||
raise CommandError(
|
||||
"couldn't handle %s template %s." % (self.app_or_project, template)
|
||||
)
|
||||
|
||||
def validate_name(self, name, name_or_dir="name"):
|
||||
if name is None:
|
||||
raise CommandError(
|
||||
"you must provide {an} {app} name".format(
|
||||
an=self.a_or_an,
|
||||
app=self.app_or_project,
|
||||
)
|
||||
)
|
||||
# Check it's a valid directory name.
|
||||
if not name.isidentifier():
|
||||
raise CommandError(
|
||||
"'{name}' is not a valid {app} {type}. Please make sure the "
|
||||
"{type} is a valid identifier.".format(
|
||||
name=name,
|
||||
app=self.app_or_project,
|
||||
type=name_or_dir,
|
||||
)
|
||||
)
|
||||
# Check that __spec__ doesn't exist.
|
||||
if find_spec(name) is not None:
|
||||
raise CommandError(
|
||||
"'{name}' conflicts with the name of an existing Python "
|
||||
"module and cannot be used as {an} {app} {type}. Please try "
|
||||
"another {type}.".format(
|
||||
name=name,
|
||||
an=self.a_or_an,
|
||||
app=self.app_or_project,
|
||||
type=name_or_dir,
|
||||
)
|
||||
)
|
||||
|
||||
def download(self, url):
|
||||
"""
|
||||
Download the given URL and return the file name.
|
||||
"""
|
||||
|
||||
def cleanup_url(url):
|
||||
tmp = url.rstrip("/")
|
||||
filename = tmp.split("/")[-1]
|
||||
if url.endswith("/"):
|
||||
display_url = tmp + "/"
|
||||
else:
|
||||
display_url = url
|
||||
return filename, display_url
|
||||
|
||||
prefix = "django_%s_template_" % self.app_or_project
|
||||
tempdir = tempfile.mkdtemp(prefix=prefix, suffix="_download")
|
||||
self.paths_to_remove.append(tempdir)
|
||||
filename, display_url = cleanup_url(url)
|
||||
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Downloading %s" % display_url)
|
||||
|
||||
the_path = os.path.join(tempdir, filename)
|
||||
opener = build_opener()
|
||||
opener.addheaders = [("User-Agent", f"Django/{django.__version__}")]
|
||||
try:
|
||||
with opener.open(url) as source, open(the_path, "wb") as target:
|
||||
headers = source.info()
|
||||
target.write(source.read())
|
||||
except OSError as e:
|
||||
raise CommandError(
|
||||
"couldn't download URL %s to %s: %s" % (url, filename, e)
|
||||
)
|
||||
|
||||
used_name = the_path.split("/")[-1]
|
||||
|
||||
# Trying to get better name from response headers
|
||||
content_disposition = headers["content-disposition"]
|
||||
if content_disposition:
|
||||
_, params = parse_header_parameters(content_disposition)
|
||||
guessed_filename = params.get("filename") or used_name
|
||||
else:
|
||||
guessed_filename = used_name
|
||||
|
||||
# Falling back to content type guessing
|
||||
ext = self.splitext(guessed_filename)[1]
|
||||
content_type = headers["content-type"]
|
||||
if not ext and content_type:
|
||||
ext = mimetypes.guess_extension(content_type)
|
||||
if ext:
|
||||
guessed_filename += ext
|
||||
|
||||
# Move the temporary file to a filename that has better
|
||||
# chances of being recognized by the archive utils
|
||||
if used_name != guessed_filename:
|
||||
guessed_path = os.path.join(tempdir, guessed_filename)
|
||||
shutil.move(the_path, guessed_path)
|
||||
return guessed_path
|
||||
|
||||
# Giving up
|
||||
return the_path
|
||||
|
||||
def splitext(self, the_path):
|
||||
"""
|
||||
Like os.path.splitext, but takes off .tar, too
|
||||
"""
|
||||
base, ext = posixpath.splitext(the_path)
|
||||
if base.lower().endswith(".tar"):
|
||||
ext = base[-4:] + ext
|
||||
base = base[:-4]
|
||||
return base, ext
|
||||
|
||||
def extract(self, filename):
|
||||
"""
|
||||
Extract the given file to a temporary directory and return
|
||||
the path of the directory with the extracted content.
|
||||
"""
|
||||
prefix = "django_%s_template_" % self.app_or_project
|
||||
tempdir = tempfile.mkdtemp(prefix=prefix, suffix="_extract")
|
||||
self.paths_to_remove.append(tempdir)
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Extracting %s" % filename)
|
||||
try:
|
||||
archive.extract(filename, tempdir)
|
||||
return tempdir
|
||||
except (archive.ArchiveException, OSError) as e:
|
||||
raise CommandError(
|
||||
"couldn't extract file %s to %s: %s" % (filename, tempdir, e)
|
||||
)
|
||||
|
||||
def is_url(self, template):
|
||||
"""Return True if the name looks like a URL."""
|
||||
if ":" not in template:
|
||||
return False
|
||||
scheme = template.split(":", 1)[0].lower()
|
||||
return scheme in self.url_schemes
|
||||
|
||||
def apply_umask(self, old_path, new_path):
|
||||
current_umask = os.umask(0)
|
||||
os.umask(current_umask)
|
||||
current_mode = stat.S_IMODE(os.stat(old_path).st_mode)
|
||||
os.chmod(new_path, current_mode & ~current_umask)
|
||||
|
||||
def make_writeable(self, filename):
|
||||
"""
|
||||
Make sure that the file is writeable.
|
||||
Useful if our source is read-only.
|
||||
"""
|
||||
if not os.access(filename, os.W_OK):
|
||||
st = os.stat(filename)
|
||||
new_permissions = stat.S_IMODE(st.st_mode) | stat.S_IWUSR
|
||||
os.chmod(filename, new_permissions)
|
||||
181
.venv/Lib/site-packages/django/core/management/utils.py
Normal file
181
.venv/Lib/site-packages/django/core/management/utils.py
Normal file
@@ -0,0 +1,181 @@
|
||||
import fnmatch
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
from django.apps import apps as installed_apps
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.encoding import DEFAULT_LOCALE_ENCODING
|
||||
|
||||
from .base import CommandError, CommandParser
|
||||
|
||||
|
||||
def popen_wrapper(args, stdout_encoding="utf-8"):
|
||||
"""
|
||||
Friendly wrapper around Popen.
|
||||
|
||||
Return stdout output, stderr output, and OS status code.
|
||||
"""
|
||||
try:
|
||||
p = run(args, capture_output=True, close_fds=os.name != "nt")
|
||||
except OSError as err:
|
||||
raise CommandError("Error executing %s" % args[0]) from err
|
||||
return (
|
||||
p.stdout.decode(stdout_encoding),
|
||||
p.stderr.decode(DEFAULT_LOCALE_ENCODING, errors="replace"),
|
||||
p.returncode,
|
||||
)
|
||||
|
||||
|
||||
def handle_extensions(extensions):
|
||||
"""
|
||||
Organize multiple extensions that are separated with commas or passed by
|
||||
using --extension/-e multiple times.
|
||||
|
||||
For example: running 'django-admin makemessages -e js,txt -e xhtml -a'
|
||||
would result in an extension list: ['.js', '.txt', '.xhtml']
|
||||
|
||||
>>> handle_extensions(['.html', 'html,js,py,py,py,.py', 'py,.py'])
|
||||
{'.html', '.js', '.py'}
|
||||
>>> handle_extensions(['.html, txt,.tpl'])
|
||||
{'.html', '.tpl', '.txt'}
|
||||
"""
|
||||
ext_list = []
|
||||
for ext in extensions:
|
||||
ext_list.extend(ext.replace(" ", "").split(","))
|
||||
for i, ext in enumerate(ext_list):
|
||||
if not ext.startswith("."):
|
||||
ext_list[i] = ".%s" % ext_list[i]
|
||||
return set(ext_list)
|
||||
|
||||
|
||||
def find_command(cmd, path=None, pathext=None):
|
||||
if path is None:
|
||||
path = os.environ.get("PATH", "").split(os.pathsep)
|
||||
if isinstance(path, str):
|
||||
path = [path]
|
||||
# check if there are funny path extensions for executables, e.g. Windows
|
||||
if pathext is None:
|
||||
pathext = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(os.pathsep)
|
||||
# don't use extensions if the command ends with one of them
|
||||
for ext in pathext:
|
||||
if cmd.endswith(ext):
|
||||
pathext = [""]
|
||||
break
|
||||
# check if we find the command on PATH
|
||||
for p in path:
|
||||
f = os.path.join(p, cmd)
|
||||
if os.path.isfile(f):
|
||||
return f
|
||||
for ext in pathext:
|
||||
fext = f + ext
|
||||
if os.path.isfile(fext):
|
||||
return fext
|
||||
return None
|
||||
|
||||
|
||||
def get_random_secret_key():
|
||||
"""
|
||||
Return a 50 character random string usable as a SECRET_KEY setting value.
|
||||
"""
|
||||
chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
|
||||
return get_random_string(50, chars)
|
||||
|
||||
|
||||
def parse_apps_and_model_labels(labels):
|
||||
"""
|
||||
Parse a list of "app_label.ModelName" or "app_label" strings into actual
|
||||
objects and return a two-element tuple:
|
||||
(set of model classes, set of app_configs).
|
||||
Raise a CommandError if some specified models or apps don't exist.
|
||||
"""
|
||||
apps = set()
|
||||
models = set()
|
||||
|
||||
for label in labels:
|
||||
if "." in label:
|
||||
try:
|
||||
model = installed_apps.get_model(label)
|
||||
except LookupError:
|
||||
raise CommandError("Unknown model: %s" % label)
|
||||
models.add(model)
|
||||
else:
|
||||
try:
|
||||
app_config = installed_apps.get_app_config(label)
|
||||
except LookupError as e:
|
||||
raise CommandError(str(e))
|
||||
apps.add(app_config)
|
||||
|
||||
return models, apps
|
||||
|
||||
|
||||
def get_command_line_option(argv, option):
|
||||
"""
|
||||
Return the value of a command line option (which should include leading
|
||||
dashes, e.g. '--testrunner') from an argument list. Return None if the
|
||||
option wasn't passed or if the argument list couldn't be parsed.
|
||||
"""
|
||||
parser = CommandParser(add_help=False, allow_abbrev=False)
|
||||
parser.add_argument(option, dest="value")
|
||||
try:
|
||||
options, _ = parser.parse_known_args(argv[2:])
|
||||
except CommandError:
|
||||
return None
|
||||
else:
|
||||
return options.value
|
||||
|
||||
|
||||
def normalize_path_patterns(patterns):
|
||||
"""Normalize an iterable of glob style patterns based on OS."""
|
||||
patterns = [os.path.normcase(p) for p in patterns]
|
||||
dir_suffixes = {"%s*" % path_sep for path_sep in {"/", os.sep}}
|
||||
norm_patterns = []
|
||||
for pattern in patterns:
|
||||
for dir_suffix in dir_suffixes:
|
||||
if pattern.endswith(dir_suffix):
|
||||
norm_patterns.append(pattern.removesuffix(dir_suffix))
|
||||
break
|
||||
else:
|
||||
norm_patterns.append(pattern)
|
||||
return norm_patterns
|
||||
|
||||
|
||||
def is_ignored_path(path, ignore_patterns):
|
||||
"""
|
||||
Check if the given path should be ignored or not based on matching
|
||||
one of the glob style `ignore_patterns`.
|
||||
"""
|
||||
path = Path(path)
|
||||
|
||||
def ignore(pattern):
|
||||
return fnmatch.fnmatchcase(path.name, pattern) or fnmatch.fnmatchcase(
|
||||
str(path), pattern
|
||||
)
|
||||
|
||||
return any(ignore(pattern) for pattern in normalize_path_patterns(ignore_patterns))
|
||||
|
||||
|
||||
def find_formatters():
|
||||
return {"black_path": shutil.which("black")}
|
||||
|
||||
|
||||
def run_formatters(written_files, black_path=(sentinel := object()), stderr=sys.stderr):
|
||||
"""
|
||||
Run the black formatter on the specified files.
|
||||
"""
|
||||
# Use a sentinel rather than None, as which() returns None when not found.
|
||||
if black_path is sentinel:
|
||||
black_path = shutil.which("black")
|
||||
if black_path:
|
||||
try:
|
||||
subprocess.run(
|
||||
[black_path, "--fast", "--", *written_files],
|
||||
capture_output=True,
|
||||
)
|
||||
except OSError:
|
||||
stderr.write("Formatters failed to launch:")
|
||||
traceback.print_exc(file=stderr)
|
||||
Reference in New Issue
Block a user