1"""
2Base classes for writing management commands (named commands which can
3be executed through ``django-admin`` or ``manage.py``).
4"""
5
6import argparse
7import os
8import sys
9from argparse import ArgumentParser, HelpFormatter
10from functools import partial
11from io import TextIOBase
12
13import django
14from django.core import checks
15from django.core.exceptions import ImproperlyConfigured
16from django.core.management.color import color_style, no_style
17from django.db import DEFAULT_DB_ALIAS, connections
18
19ALL_CHECKS = "__all__"
20
21
22class CommandError(Exception):
23 """
24 Exception class indicating a problem while executing a management
25 command.
26
27 If this exception is raised during the execution of a management
28 command, it will be caught and turned into a nicely-printed error
29 message to the appropriate output stream (i.e., stderr); as a
30 result, raising this exception (with a sensible description of the
31 error) is the preferred way to indicate that something has gone
32 wrong in the execution of a command.
33 """
34
35 def __init__(self, *args, returncode=1, **kwargs):
36 self.returncode = returncode
37 super().__init__(*args, **kwargs)
38
39
40class SystemCheckError(CommandError):
41 """
42 The system check framework detected unrecoverable errors.
43 """
44
45 pass
46
47
48class CommandParser(ArgumentParser):
49 """
50 Customized ArgumentParser class to improve some error messages and prevent
51 SystemExit in several occasions, as SystemExit is unacceptable when a
52 command is called programmatically.
53 """
54
55 def __init__(
56 self, *, missing_args_message=None, called_from_command_line=None, **kwargs
57 ):
58 self.missing_args_message = missing_args_message
59 self.called_from_command_line = called_from_command_line
60 super().__init__(**kwargs)
61
62 def parse_args(self, args=None, namespace=None):
63 # Catch missing argument for a better error message
64 if self.missing_args_message and not (
65 args or any(not arg.startswith("-") for arg in args)
66 ):
67 self.error(self.missing_args_message)
68 return super().parse_args(args, namespace)
69
70 def error(self, message):
71 if self.called_from_command_line:
72 super().error(message)
73 else:
74 raise CommandError("Error: %s" % message)
75
76 def add_subparsers(self, **kwargs):
77 parser_class = kwargs.get("parser_class", type(self))
78 if issubclass(parser_class, CommandParser):
79 kwargs["parser_class"] = partial(
80 parser_class,
81 called_from_command_line=self.called_from_command_line,
82 )
83 return super().add_subparsers(**kwargs)
84
85
86def handle_default_options(options):
87 """
88 Include any default options that all commands should accept here
89 so that ManagementUtility can handle them before searching for
90 user commands.
91 """
92 if options.settings:
93 os.environ["DJANGO_SETTINGS_MODULE"] = options.settings
94 if options.pythonpath:
95 sys.path.insert(0, options.pythonpath)
96
97
98def no_translations(handle_func):
99 """Decorator that forces a command to run with translations deactivated."""
100
101 def wrapper(*args, **kwargs):
102 from django.utils import translation
103
104 saved_locale = translation.get_language()
105 translation.deactivate_all()
106 try:
107 res = handle_func(*args, **kwargs)
108 finally:
109 if saved_locale is not None:
110 translation.activate(saved_locale)
111 return res
112
113 return wrapper
114
115
116class DjangoHelpFormatter(HelpFormatter):
117 """
118 Customized formatter so that command-specific arguments appear in the
119 --help output before arguments common to all commands.
120 """
121
122 show_last = {
123 "--version",
124 "--verbosity",
125 "--traceback",
126 "--settings",
127 "--pythonpath",
128 "--no-color",
129 "--force-color",
130 "--skip-checks",
131 }
132
133 def _reordered_actions(self, actions):
134 return sorted(
135 actions, key=lambda a: set(a.option_strings) & self.show_last != set()
136 )
137
138 def add_usage(self, usage, actions, *args, **kwargs):
139 super().add_usage(usage, self._reordered_actions(actions), *args, **kwargs)
140
141 def add_arguments(self, actions):
142 super().add_arguments(self._reordered_actions(actions))
143
144
145class OutputWrapper:
146 """
147 Wrapper around stdout/stderr
148 """
149
150 @property
151 def style_func(self):
152 return self._style_func
153
154 @style_func.setter
155 def style_func(self, style_func):
156 if style_func and self.isatty():
157 self._style_func = style_func
158 else:
159 self._style_func = lambda x: x
160
161 def __init__(self, out, ending="\n"):
162 self._out = out
163 self.style_func = None
164 self.ending = ending
165
166 def __getattr__(self, name):
167 return getattr(self._out, name)
168
169 def flush(self):
170 if hasattr(self._out, "flush"):
171 self._out.flush()
172
173 def isatty(self):
174 return hasattr(self._out, "isatty") and self._out.isatty()
175
176 def write(self, msg="", style_func=None, ending=None):
177 ending = self.ending if ending is None else ending
178 if ending and not msg.endswith(ending):
179 msg += ending
180 style_func = style_func or self.style_func
181 self._out.write(style_func(msg))
182
183
184TextIOBase.register(OutputWrapper)
185
186
187class BaseCommand:
188 """
189 The base class from which all management commands ultimately
190 derive.
191
192 Use this class if you want access to all of the mechanisms which
193 parse the command-line arguments and work out what code to call in
194 response; if you don't need to change any of that behavior,
195 consider using one of the subclasses defined in this file.
196
197 If you are interested in overriding/customizing various aspects of
198 the command-parsing and -execution behavior, the normal flow works
199 as follows:
200
201 1. ``django-admin`` or ``manage.py`` loads the command class
202 and calls its ``run_from_argv()`` method.
203
204 2. The ``run_from_argv()`` method calls ``create_parser()`` to get
205 an ``ArgumentParser`` for the arguments, parses them, performs
206 any environment changes requested by options like
207 ``pythonpath``, and then calls the ``execute()`` method,
208 passing the parsed arguments.
209
210 3. The ``execute()`` method attempts to carry out the command by
211 calling the ``handle()`` method with the parsed arguments; any
212 output produced by ``handle()`` will be printed to standard
213 output and, if the command is intended to produce a block of
214 SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``.
215
216 4. If ``handle()`` or ``execute()`` raised any exception (e.g.
217 ``CommandError``), ``run_from_argv()`` will instead print an error
218 message to ``stderr``.
219
220 Thus, the ``handle()`` method is typically the starting point for
221 subclasses; many built-in commands and command types either place
222 all of their logic in ``handle()``, or perform some additional
223 parsing work in ``handle()`` and then delegate from it to more
224 specialized methods as needed.
225
226 Several attributes affect behavior at various steps along the way:
227
228 ``help``
229 A short description of the command, which will be printed in
230 help messages.
231
232 ``output_transaction``
233 A boolean indicating whether the command outputs SQL
234 statements; if ``True``, the output will automatically be
235 wrapped with ``BEGIN;`` and ``COMMIT;``. Default value is
236 ``False``.
237
238 ``requires_migrations_checks``
239 A boolean; if ``True``, the command prints a warning if the set of
240 migrations on disk don't match the migrations in the database.
241
242 ``requires_system_checks``
243 A list or tuple of tags, e.g. [Tags.staticfiles, Tags.models]. System
244 checks registered in the chosen tags will be checked for errors prior
245 to executing the command. The value '__all__' can be used to specify
246 that all system checks should be performed. Default value is '__all__'.
247
248 To validate an individual application's models
249 rather than all applications' models, call
250 ``self.check(app_configs)`` from ``handle()``, where ``app_configs``
251 is the list of application's configuration provided by the
252 app registry.
253
254 ``stealth_options``
255 A tuple of any options the command uses which aren't defined by the
256 argument parser.
257 """
258
259 # Metadata about this command.
260 help = ""
261
262 # Configuration shortcuts that alter various logic.
263 _called_from_command_line = False
264 output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
265 requires_migrations_checks = False
266 requires_system_checks = "__all__"
267 # Arguments, common to all commands, which aren't defined by the argument
268 # parser.
269 base_stealth_options = ("stderr", "stdout")
270 # Command-specific options not defined by the argument parser.
271 stealth_options = ()
272 suppressed_base_arguments = set()
273
274 def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
275 self.stdout = OutputWrapper(stdout or sys.stdout)
276 self.stderr = OutputWrapper(stderr or sys.stderr)
277 if no_color and force_color:
278 raise CommandError("'no_color' and 'force_color' can't be used together.")
279 if no_color:
280 self.style = no_style()
281 else:
282 self.style = color_style(force_color)
283 self.stderr.style_func = self.style.ERROR
284 if (
285 not isinstance(self.requires_system_checks, (list, tuple))
286 and self.requires_system_checks != ALL_CHECKS
287 ):
288 raise TypeError("requires_system_checks must be a list or tuple.")
289
290 def get_version(self):
291 """
292 Return the Django version, which should be correct for all built-in
293 Django commands. User-supplied commands can override this method to
294 return their own version.
295 """
296 return django.get_version()
297
298 def create_parser(self, prog_name, subcommand, **kwargs):
299 """
300 Create and return the ``ArgumentParser`` which will be used to
301 parse the arguments to this command.
302 """
303 kwargs.setdefault("formatter_class", DjangoHelpFormatter)
304 parser = CommandParser(
305 prog="%s %s" % (os.path.basename(prog_name), subcommand),
306 description=self.help or None,
307 missing_args_message=getattr(self, "missing_args_message", None),
308 called_from_command_line=getattr(self, "_called_from_command_line", None),
309 **kwargs,
310 )
311 self.add_base_argument(
312 parser,
313 "--version",
314 action="version",
315 version=self.get_version(),
316 help="Show program's version number and exit.",
317 )
318 self.add_base_argument(
319 parser,
320 "-v",
321 "--verbosity",
322 default=1,
323 type=int,
324 choices=[0, 1, 2, 3],
325 help=(
326 "Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, "
327 "3=very verbose output"
328 ),
329 )
330 self.add_base_argument(
331 parser,
332 "--settings",
333 help=(
334 "The Python path to a settings module, e.g. "
335 '"myproject.settings.main". If this isn\'t provided, the '
336 "DJANGO_SETTINGS_MODULE environment variable will be used."
337 ),
338 )
339 self.add_base_argument(
340 parser,
341 "--pythonpath",
342 help=(
343 "A directory to add to the Python path, e.g. "
344 '"/home/djangoprojects/myproject".'
345 ),
346 )
347 self.add_base_argument(
348 parser,
349 "--traceback",
350 action="store_true",
351 help="Display a full stack trace on CommandError exceptions.",
352 )
353 self.add_base_argument(
354 parser,
355 "--no-color",
356 action="store_true",
357 help="Don't colorize the command output.",
358 )
359 self.add_base_argument(
360 parser,
361 "--force-color",
362 action="store_true",
363 help="Force colorization of the command output.",
364 )
365 if self.requires_system_checks:
366 parser.add_argument(
367 "--skip-checks",
368 action="store_true",
369 help="Skip system checks.",
370 )
371 self.add_arguments(parser)
372 return parser
373
374 def add_arguments(self, parser):
375 """
376 Entry point for subclassed commands to add custom arguments.
377 """
378 pass
379
380 def add_base_argument(self, parser, *args, **kwargs):
381 """
382 Call the parser's add_argument() method, suppressing the help text
383 according to BaseCommand.suppressed_base_arguments.
384 """
385 for arg in args:
386 if arg in self.suppressed_base_arguments:
387 kwargs["help"] = argparse.SUPPRESS
388 break
389 parser.add_argument(*args, **kwargs)
390
391 def print_help(self, prog_name, subcommand):
392 """
393 Print the help message for this command, derived from
394 ``self.usage()``.
395 """
396 parser = self.create_parser(prog_name, subcommand)
397 parser.print_help()
398
399 def run_from_argv(self, argv):
400 """
401 Set up any environment changes requested (e.g., Python path
402 and Django settings), then run this command. If the
403 command raises a ``CommandError``, intercept it and print it sensibly
404 to stderr. If the ``--traceback`` option is present or the raised
405 ``Exception`` is not ``CommandError``, raise it.
406 """
407 self._called_from_command_line = True
408 parser = self.create_parser(argv[0], argv[1])
409
410 options = parser.parse_args(argv[2:])
411 cmd_options = vars(options)
412 # Move positional args out of options to mimic legacy optparse
413 args = cmd_options.pop("args", ())
414 handle_default_options(options)
415 try:
416 self.execute(*args, **cmd_options)
417 except CommandError as e:
418 if options.traceback:
419 raise
420
421 # SystemCheckError takes care of its own formatting.
422 if isinstance(e, SystemCheckError):
423 self.stderr.write(str(e), lambda x: x)
424 else:
425 self.stderr.write("%s: %s" % (e.__class__.__name__, e))
426 sys.exit(e.returncode)
427 finally:
428 try:
429 connections.close_all()
430 except ImproperlyConfigured:
431 # Ignore if connections aren't setup at this point (e.g. no
432 # configured settings).
433 pass
434
435 def execute(self, *args, **options):
436 """
437 Try to execute this command, performing system checks if needed (as
438 controlled by the ``requires_system_checks`` attribute, except if
439 force-skipped).
440 """
441 if options["force_color"] and options["no_color"]:
442 raise CommandError(
443 "The --no-color and --force-color options can't be used together."
444 )
445 if options["force_color"]:
446 self.style = color_style(force_color=True)
447 elif options["no_color"]:
448 self.style = no_style()
449 self.stderr.style_func = None
450 if options.get("stdout"):
451 self.stdout = OutputWrapper(options["stdout"])
452 if options.get("stderr"):
453 self.stderr = OutputWrapper(options["stderr"])
454
455 if self.requires_system_checks and not options["skip_checks"]:
456 check_kwargs = self.get_check_kwargs(options)
457 self.check(**check_kwargs)
458 if self.requires_migrations_checks:
459 self.check_migrations()
460 output = self.handle(*args, **options)
461 if output:
462 if self.output_transaction:
463 connection = connections[options.get("database", DEFAULT_DB_ALIAS)]
464 output = "%s\n%s\n%s" % (
465 self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
466 output,
467 self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
468 )
469 self.stdout.write(output)
470 return output
471
472 def get_check_kwargs(self, options):
473 if self.requires_system_checks == ALL_CHECKS:
474 return {}
475 return {"tags": self.requires_system_checks}
476
477 def check(
478 self,
479 app_configs=None,
480 tags=None,
481 display_num_errors=False,
482 include_deployment_checks=False,
483 fail_level=checks.ERROR,
484 databases=None,
485 ):
486 """
487 Use the system check framework to validate entire Django project.
488 Raise CommandError for any serious message (error or critical errors).
489 If there are only light messages (like warnings), print them to stderr
490 and don't raise an exception.
491 """
492 all_issues = checks.run_checks(
493 app_configs=app_configs,
494 tags=tags,
495 include_deployment_checks=include_deployment_checks,
496 databases=databases,
497 )
498
499 header, body, footer = "", "", ""
500 visible_issue_count = 0 # excludes silenced warnings
501
502 if all_issues:
503 debugs = [
504 e for e in all_issues if e.level < checks.INFO and not e.is_silenced()
505 ]
506 infos = [
507 e
508 for e in all_issues
509 if checks.INFO <= e.level < checks.WARNING and not e.is_silenced()
510 ]
511 warnings = [
512 e
513 for e in all_issues
514 if checks.WARNING <= e.level < checks.ERROR and not e.is_silenced()
515 ]
516 errors = [
517 e
518 for e in all_issues
519 if checks.ERROR <= e.level < checks.CRITICAL and not e.is_silenced()
520 ]
521 criticals = [
522 e
523 for e in all_issues
524 if checks.CRITICAL <= e.level and not e.is_silenced()
525 ]
526 sorted_issues = [
527 (criticals, "CRITICALS"),
528 (errors, "ERRORS"),
529 (warnings, "WARNINGS"),
530 (infos, "INFOS"),
531 (debugs, "DEBUGS"),
532 ]
533
534 for issues, group_name in sorted_issues:
535 if issues:
536 visible_issue_count += len(issues)
537 formatted = (
538 (
539 self.style.ERROR(str(e))
540 if e.is_serious()
541 else self.style.WARNING(str(e))
542 )
543 for e in issues
544 )
545 formatted = "\n".join(sorted(formatted))
546 body += "\n%s:\n%s\n" % (group_name, formatted)
547
548 if visible_issue_count:
549 header = "System check identified some issues:\n"
550
551 if display_num_errors:
552 if visible_issue_count:
553 footer += "\n"
554 footer += "System check identified %s (%s silenced)." % (
555 (
556 "no issues"
557 if visible_issue_count == 0
558 else (
559 "1 issue"
560 if visible_issue_count == 1
561 else "%s issues" % visible_issue_count
562 )
563 ),
564 len(all_issues) - visible_issue_count,
565 )
566
567 if any(e.is_serious(fail_level) and not e.is_silenced() for e in all_issues):
568 msg = self.style.ERROR("SystemCheckError: %s" % header) + body + footer
569 raise SystemCheckError(msg)
570 else:
571 msg = header + body + footer
572
573 if msg:
574 if visible_issue_count:
575 self.stderr.write(msg, lambda x: x)
576 else:
577 self.stdout.write(msg)
578
579 def check_migrations(self):
580 """
581 Print a warning if the set of migrations on disk don't match the
582 migrations in the database.
583 """
584 from django.db.migrations.executor import MigrationExecutor
585
586 try:
587 executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
588 except ImproperlyConfigured:
589 # No databases are configured (or the dummy one)
590 return
591
592 plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
593 if plan:
594 apps_waiting_migration = sorted(
595 {migration.app_label for migration, backwards in plan}
596 )
597 self.stdout.write(
598 self.style.NOTICE(
599 "\nYou have %(unapplied_migration_count)s unapplied migration(s). "
600 "Your project may not work properly until you apply the "
601 "migrations for app(s): %(apps_waiting_migration)s."
602 % {
603 "unapplied_migration_count": len(plan),
604 "apps_waiting_migration": ", ".join(apps_waiting_migration),
605 }
606 )
607 )
608 self.stdout.write(
609 self.style.NOTICE("Run 'python manage.py migrate' to apply them.")
610 )
611
612 def handle(self, *args, **options):
613 """
614 The actual logic of the command. Subclasses must implement
615 this method.
616 """
617 raise NotImplementedError(
618 "subclasses of BaseCommand must provide a handle() method"
619 )
620
621
622class AppCommand(BaseCommand):
623 """
624 A management command which takes one or more installed application labels
625 as arguments, and does something with each of them.
626
627 Rather than implementing ``handle()``, subclasses must implement
628 ``handle_app_config()``, which will be called once for each application.
629 """
630
631 missing_args_message = "Enter at least one application label."
632
633 def add_arguments(self, parser):
634 parser.add_argument(
635 "args",
636 metavar="app_label",
637 nargs="+",
638 help="One or more application label.",
639 )
640
641 def handle(self, *app_labels, **options):
642 from django.apps import apps
643
644 try:
645 app_configs = [apps.get_app_config(app_label) for app_label in app_labels]
646 except (LookupError, ImportError) as e:
647 raise CommandError(
648 "%s. Are you sure your INSTALLED_APPS setting is correct?" % e
649 )
650 output = []
651 for app_config in app_configs:
652 app_output = self.handle_app_config(app_config, **options)
653 if app_output:
654 output.append(app_output)
655 return "\n".join(output)
656
657 def handle_app_config(self, app_config, **options):
658 """
659 Perform the command's actions for app_config, an AppConfig instance
660 corresponding to an application label given on the command line.
661 """
662 raise NotImplementedError(
663 "Subclasses of AppCommand must provide a handle_app_config() method."
664 )
665
666
667class LabelCommand(BaseCommand):
668 """
669 A management command which takes one or more arbitrary arguments
670 (labels) on the command line, and does something with each of
671 them.
672
673 Rather than implementing ``handle()``, subclasses must implement
674 ``handle_label()``, which will be called once for each label.
675
676 If the arguments should be names of installed applications, use
677 ``AppCommand`` instead.
678 """
679
680 label = "label"
681 missing_args_message = "Enter at least one %s."
682
683 def __init__(self, *args, **kwargs):
684 super().__init__(*args, **kwargs)
685
686 if self.missing_args_message == LabelCommand.missing_args_message:
687 self.missing_args_message = self.missing_args_message % self.label
688
689 def add_arguments(self, parser):
690 parser.add_argument("args", metavar=self.label, nargs="+")
691
692 def handle(self, *labels, **options):
693 output = []
694 for label in labels:
695 label_output = self.handle_label(label, **options)
696 if label_output:
697 output.append(label_output)
698 return "\n".join(output)
699
700 def handle_label(self, label, **options):
701 """
702 Perform the command's actions for ``label``, which will be the
703 string as given on the command line.
704 """
705 raise NotImplementedError(
706 "subclasses of LabelCommand must provide a handle_label() method"
707 )