You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. """
  2. Base classes for writing management commands (named commands which can
  3. be executed through ``django-admin`` or ``manage.py``).
  4. """
  5. import os
  6. import sys
  7. from argparse import ArgumentParser, HelpFormatter
  8. from io import TextIOBase
  9. import django
  10. from django.core import checks
  11. from django.core.exceptions import ImproperlyConfigured
  12. from django.core.management.color import color_style, no_style
  13. from django.db import DEFAULT_DB_ALIAS, connections
  14. class CommandError(Exception):
  15. """
  16. Exception class indicating a problem while executing a management
  17. command.
  18. If this exception is raised during the execution of a management
  19. command, it will be caught and turned into a nicely-printed error
  20. message to the appropriate output stream (i.e., stderr); as a
  21. result, raising this exception (with a sensible description of the
  22. error) is the preferred way to indicate that something has gone
  23. wrong in the execution of a command.
  24. """
  25. pass
  26. class SystemCheckError(CommandError):
  27. """
  28. The system check framework detected unrecoverable errors.
  29. """
  30. pass
  31. class CommandParser(ArgumentParser):
  32. """
  33. Customized ArgumentParser class to improve some error messages and prevent
  34. SystemExit in several occasions, as SystemExit is unacceptable when a
  35. command is called programmatically.
  36. """
  37. def __init__(self, *, missing_args_message=None, called_from_command_line=None, **kwargs):
  38. self.missing_args_message = missing_args_message
  39. self.called_from_command_line = called_from_command_line
  40. super().__init__(**kwargs)
  41. def parse_args(self, args=None, namespace=None):
  42. # Catch missing argument for a better error message
  43. if (self.missing_args_message and
  44. not (args or any(not arg.startswith('-') for arg in args))):
  45. self.error(self.missing_args_message)
  46. return super().parse_args(args, namespace)
  47. def error(self, message):
  48. if self.called_from_command_line:
  49. super().error(message)
  50. else:
  51. raise CommandError("Error: %s" % message)
  52. def handle_default_options(options):
  53. """
  54. Include any default options that all commands should accept here
  55. so that ManagementUtility can handle them before searching for
  56. user commands.
  57. """
  58. if options.settings:
  59. os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
  60. if options.pythonpath:
  61. sys.path.insert(0, options.pythonpath)
  62. def no_translations(handle_func):
  63. """Decorator that forces a command to run with translations deactivated."""
  64. def wrapped(*args, **kwargs):
  65. from django.utils import translation
  66. saved_locale = translation.get_language()
  67. translation.deactivate_all()
  68. try:
  69. res = handle_func(*args, **kwargs)
  70. finally:
  71. if saved_locale is not None:
  72. translation.activate(saved_locale)
  73. return res
  74. return wrapped
  75. class DjangoHelpFormatter(HelpFormatter):
  76. """
  77. Customized formatter so that command-specific arguments appear in the
  78. --help output before arguments common to all commands.
  79. """
  80. show_last = {
  81. '--version', '--verbosity', '--traceback', '--settings', '--pythonpath',
  82. '--no-color', '--force-color',
  83. }
  84. def _reordered_actions(self, actions):
  85. return sorted(
  86. actions,
  87. key=lambda a: set(a.option_strings) & self.show_last != set()
  88. )
  89. def add_usage(self, usage, actions, *args, **kwargs):
  90. super().add_usage(usage, self._reordered_actions(actions), *args, **kwargs)
  91. def add_arguments(self, actions):
  92. super().add_arguments(self._reordered_actions(actions))
  93. class OutputWrapper(TextIOBase):
  94. """
  95. Wrapper around stdout/stderr
  96. """
  97. @property
  98. def style_func(self):
  99. return self._style_func
  100. @style_func.setter
  101. def style_func(self, style_func):
  102. if style_func and self.isatty():
  103. self._style_func = style_func
  104. else:
  105. self._style_func = lambda x: x
  106. def __init__(self, out, style_func=None, ending='\n'):
  107. self._out = out
  108. self.style_func = None
  109. self.ending = ending
  110. def __getattr__(self, name):
  111. return getattr(self._out, name)
  112. def isatty(self):
  113. return hasattr(self._out, 'isatty') and self._out.isatty()
  114. def write(self, msg, style_func=None, ending=None):
  115. ending = self.ending if ending is None else ending
  116. if ending and not msg.endswith(ending):
  117. msg += ending
  118. style_func = style_func or self.style_func
  119. self._out.write(style_func(msg))
  120. class BaseCommand:
  121. """
  122. The base class from which all management commands ultimately
  123. derive.
  124. Use this class if you want access to all of the mechanisms which
  125. parse the command-line arguments and work out what code to call in
  126. response; if you don't need to change any of that behavior,
  127. consider using one of the subclasses defined in this file.
  128. If you are interested in overriding/customizing various aspects of
  129. the command-parsing and -execution behavior, the normal flow works
  130. as follows:
  131. 1. ``django-admin`` or ``manage.py`` loads the command class
  132. and calls its ``run_from_argv()`` method.
  133. 2. The ``run_from_argv()`` method calls ``create_parser()`` to get
  134. an ``ArgumentParser`` for the arguments, parses them, performs
  135. any environment changes requested by options like
  136. ``pythonpath``, and then calls the ``execute()`` method,
  137. passing the parsed arguments.
  138. 3. The ``execute()`` method attempts to carry out the command by
  139. calling the ``handle()`` method with the parsed arguments; any
  140. output produced by ``handle()`` will be printed to standard
  141. output and, if the command is intended to produce a block of
  142. SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``.
  143. 4. If ``handle()`` or ``execute()`` raised any exception (e.g.
  144. ``CommandError``), ``run_from_argv()`` will instead print an error
  145. message to ``stderr``.
  146. Thus, the ``handle()`` method is typically the starting point for
  147. subclasses; many built-in commands and command types either place
  148. all of their logic in ``handle()``, or perform some additional
  149. parsing work in ``handle()`` and then delegate from it to more
  150. specialized methods as needed.
  151. Several attributes affect behavior at various steps along the way:
  152. ``help``
  153. A short description of the command, which will be printed in
  154. help messages.
  155. ``output_transaction``
  156. A boolean indicating whether the command outputs SQL
  157. statements; if ``True``, the output will automatically be
  158. wrapped with ``BEGIN;`` and ``COMMIT;``. Default value is
  159. ``False``.
  160. ``requires_migrations_checks``
  161. A boolean; if ``True``, the command prints a warning if the set of
  162. migrations on disk don't match the migrations in the database.
  163. ``requires_system_checks``
  164. A boolean; if ``True``, entire Django project will be checked for errors
  165. prior to executing the command. Default value is ``True``.
  166. To validate an individual application's models
  167. rather than all applications' models, call
  168. ``self.check(app_configs)`` from ``handle()``, where ``app_configs``
  169. is the list of application's configuration provided by the
  170. app registry.
  171. ``stealth_options``
  172. A tuple of any options the command uses which aren't defined by the
  173. argument parser.
  174. """
  175. # Metadata about this command.
  176. help = ''
  177. # Configuration shortcuts that alter various logic.
  178. _called_from_command_line = False
  179. output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
  180. requires_migrations_checks = False
  181. requires_system_checks = True
  182. # Arguments, common to all commands, which aren't defined by the argument
  183. # parser.
  184. base_stealth_options = ('skip_checks', 'stderr', 'stdout')
  185. # Command-specific options not defined by the argument parser.
  186. stealth_options = ()
  187. def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False):
  188. self.stdout = OutputWrapper(stdout or sys.stdout)
  189. self.stderr = OutputWrapper(stderr or sys.stderr)
  190. if no_color and force_color:
  191. raise CommandError("'no_color' and 'force_color' can't be used together.")
  192. if no_color:
  193. self.style = no_style()
  194. else:
  195. self.style = color_style(force_color)
  196. self.stderr.style_func = self.style.ERROR
  197. def get_version(self):
  198. """
  199. Return the Django version, which should be correct for all built-in
  200. Django commands. User-supplied commands can override this method to
  201. return their own version.
  202. """
  203. return django.get_version()
  204. def create_parser(self, prog_name, subcommand, **kwargs):
  205. """
  206. Create and return the ``ArgumentParser`` which will be used to
  207. parse the arguments to this command.
  208. """
  209. parser = CommandParser(
  210. prog='%s %s' % (os.path.basename(prog_name), subcommand),
  211. description=self.help or None,
  212. formatter_class=DjangoHelpFormatter,
  213. missing_args_message=getattr(self, 'missing_args_message', None),
  214. called_from_command_line=getattr(self, '_called_from_command_line', None),
  215. **kwargs
  216. )
  217. parser.add_argument('--version', action='version', version=self.get_version())
  218. parser.add_argument(
  219. '-v', '--verbosity', default=1,
  220. type=int, choices=[0, 1, 2, 3],
  221. help='Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output',
  222. )
  223. parser.add_argument(
  224. '--settings',
  225. help=(
  226. 'The Python path to a settings module, e.g. '
  227. '"myproject.settings.main". If this isn\'t provided, the '
  228. 'DJANGO_SETTINGS_MODULE environment variable will be used.'
  229. ),
  230. )
  231. parser.add_argument(
  232. '--pythonpath',
  233. help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".',
  234. )
  235. parser.add_argument('--traceback', action='store_true', help='Raise on CommandError exceptions')
  236. parser.add_argument(
  237. '--no-color', action='store_true',
  238. help="Don't colorize the command output.",
  239. )
  240. parser.add_argument(
  241. '--force-color', action='store_true',
  242. help='Force colorization of the command output.',
  243. )
  244. self.add_arguments(parser)
  245. return parser
  246. def add_arguments(self, parser):
  247. """
  248. Entry point for subclassed commands to add custom arguments.
  249. """
  250. pass
  251. def print_help(self, prog_name, subcommand):
  252. """
  253. Print the help message for this command, derived from
  254. ``self.usage()``.
  255. """
  256. parser = self.create_parser(prog_name, subcommand)
  257. parser.print_help()
  258. def run_from_argv(self, argv):
  259. """
  260. Set up any environment changes requested (e.g., Python path
  261. and Django settings), then run this command. If the
  262. command raises a ``CommandError``, intercept it and print it sensibly
  263. to stderr. If the ``--traceback`` option is present or the raised
  264. ``Exception`` is not ``CommandError``, raise it.
  265. """
  266. self._called_from_command_line = True
  267. parser = self.create_parser(argv[0], argv[1])
  268. options = parser.parse_args(argv[2:])
  269. cmd_options = vars(options)
  270. # Move positional args out of options to mimic legacy optparse
  271. args = cmd_options.pop('args', ())
  272. handle_default_options(options)
  273. try:
  274. self.execute(*args, **cmd_options)
  275. except Exception as e:
  276. if options.traceback or not isinstance(e, CommandError):
  277. raise
  278. # SystemCheckError takes care of its own formatting.
  279. if isinstance(e, SystemCheckError):
  280. self.stderr.write(str(e), lambda x: x)
  281. else:
  282. self.stderr.write('%s: %s' % (e.__class__.__name__, e))
  283. sys.exit(1)
  284. finally:
  285. try:
  286. connections.close_all()
  287. except ImproperlyConfigured:
  288. # Ignore if connections aren't setup at this point (e.g. no
  289. # configured settings).
  290. pass
  291. def execute(self, *args, **options):
  292. """
  293. Try to execute this command, performing system checks if needed (as
  294. controlled by the ``requires_system_checks`` attribute, except if
  295. force-skipped).
  296. """
  297. if options['force_color'] and options['no_color']:
  298. raise CommandError("The --no-color and --force-color options can't be used together.")
  299. if options['force_color']:
  300. self.style = color_style(force_color=True)
  301. elif options['no_color']:
  302. self.style = no_style()
  303. self.stderr.style_func = None
  304. if options.get('stdout'):
  305. self.stdout = OutputWrapper(options['stdout'])
  306. if options.get('stderr'):
  307. self.stderr = OutputWrapper(options['stderr'], self.stderr.style_func)
  308. if self.requires_system_checks and not options.get('skip_checks'):
  309. self.check()
  310. if self.requires_migrations_checks:
  311. self.check_migrations()
  312. output = self.handle(*args, **options)
  313. if output:
  314. if self.output_transaction:
  315. connection = connections[options.get('database', DEFAULT_DB_ALIAS)]
  316. output = '%s\n%s\n%s' % (
  317. self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
  318. output,
  319. self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
  320. )
  321. self.stdout.write(output)
  322. return output
  323. def _run_checks(self, **kwargs):
  324. return checks.run_checks(**kwargs)
  325. def check(self, app_configs=None, tags=None, display_num_errors=False,
  326. include_deployment_checks=False, fail_level=checks.ERROR):
  327. """
  328. Use the system check framework to validate entire Django project.
  329. Raise CommandError for any serious message (error or critical errors).
  330. If there are only light messages (like warnings), print them to stderr
  331. and don't raise an exception.
  332. """
  333. all_issues = self._run_checks(
  334. app_configs=app_configs,
  335. tags=tags,
  336. include_deployment_checks=include_deployment_checks,
  337. )
  338. header, body, footer = "", "", ""
  339. visible_issue_count = 0 # excludes silenced warnings
  340. if all_issues:
  341. debugs = [e for e in all_issues if e.level < checks.INFO and not e.is_silenced()]
  342. infos = [e for e in all_issues if checks.INFO <= e.level < checks.WARNING and not e.is_silenced()]
  343. warnings = [e for e in all_issues if checks.WARNING <= e.level < checks.ERROR and not e.is_silenced()]
  344. errors = [e for e in all_issues if checks.ERROR <= e.level < checks.CRITICAL and not e.is_silenced()]
  345. criticals = [e for e in all_issues if checks.CRITICAL <= e.level and not e.is_silenced()]
  346. sorted_issues = [
  347. (criticals, 'CRITICALS'),
  348. (errors, 'ERRORS'),
  349. (warnings, 'WARNINGS'),
  350. (infos, 'INFOS'),
  351. (debugs, 'DEBUGS'),
  352. ]
  353. for issues, group_name in sorted_issues:
  354. if issues:
  355. visible_issue_count += len(issues)
  356. formatted = (
  357. self.style.ERROR(str(e))
  358. if e.is_serious()
  359. else self.style.WARNING(str(e))
  360. for e in issues)
  361. formatted = "\n".join(sorted(formatted))
  362. body += '\n%s:\n%s\n' % (group_name, formatted)
  363. if visible_issue_count:
  364. header = "System check identified some issues:\n"
  365. if display_num_errors:
  366. if visible_issue_count:
  367. footer += '\n'
  368. footer += "System check identified %s (%s silenced)." % (
  369. "no issues" if visible_issue_count == 0 else
  370. "1 issue" if visible_issue_count == 1 else
  371. "%s issues" % visible_issue_count,
  372. len(all_issues) - visible_issue_count,
  373. )
  374. if any(e.is_serious(fail_level) and not e.is_silenced() for e in all_issues):
  375. msg = self.style.ERROR("SystemCheckError: %s" % header) + body + footer
  376. raise SystemCheckError(msg)
  377. else:
  378. msg = header + body + footer
  379. if msg:
  380. if visible_issue_count:
  381. self.stderr.write(msg, lambda x: x)
  382. else:
  383. self.stdout.write(msg)
  384. def check_migrations(self):
  385. """
  386. Print a warning if the set of migrations on disk don't match the
  387. migrations in the database.
  388. """
  389. from django.db.migrations.executor import MigrationExecutor
  390. try:
  391. executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
  392. except ImproperlyConfigured:
  393. # No databases are configured (or the dummy one)
  394. return
  395. plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
  396. if plan:
  397. apps_waiting_migration = sorted({migration.app_label for migration, backwards in plan})
  398. self.stdout.write(
  399. self.style.NOTICE(
  400. "\nYou have %(unpplied_migration_count)s unapplied migration(s). "
  401. "Your project may not work properly until you apply the "
  402. "migrations for app(s): %(apps_waiting_migration)s." % {
  403. "unpplied_migration_count": len(plan),
  404. "apps_waiting_migration": ", ".join(apps_waiting_migration),
  405. }
  406. )
  407. )
  408. self.stdout.write(self.style.NOTICE("Run 'python manage.py migrate' to apply them.\n"))
  409. def handle(self, *args, **options):
  410. """
  411. The actual logic of the command. Subclasses must implement
  412. this method.
  413. """
  414. raise NotImplementedError('subclasses of BaseCommand must provide a handle() method')
  415. class AppCommand(BaseCommand):
  416. """
  417. A management command which takes one or more installed application labels
  418. as arguments, and does something with each of them.
  419. Rather than implementing ``handle()``, subclasses must implement
  420. ``handle_app_config()``, which will be called once for each application.
  421. """
  422. missing_args_message = "Enter at least one application label."
  423. def add_arguments(self, parser):
  424. parser.add_argument('args', metavar='app_label', nargs='+', help='One or more application label.')
  425. def handle(self, *app_labels, **options):
  426. from django.apps import apps
  427. try:
  428. app_configs = [apps.get_app_config(app_label) for app_label in app_labels]
  429. except (LookupError, ImportError) as e:
  430. raise CommandError("%s. Are you sure your INSTALLED_APPS setting is correct?" % e)
  431. output = []
  432. for app_config in app_configs:
  433. app_output = self.handle_app_config(app_config, **options)
  434. if app_output:
  435. output.append(app_output)
  436. return '\n'.join(output)
  437. def handle_app_config(self, app_config, **options):
  438. """
  439. Perform the command's actions for app_config, an AppConfig instance
  440. corresponding to an application label given on the command line.
  441. """
  442. raise NotImplementedError(
  443. "Subclasses of AppCommand must provide"
  444. "a handle_app_config() method.")
  445. class LabelCommand(BaseCommand):
  446. """
  447. A management command which takes one or more arbitrary arguments
  448. (labels) on the command line, and does something with each of
  449. them.
  450. Rather than implementing ``handle()``, subclasses must implement
  451. ``handle_label()``, which will be called once for each label.
  452. If the arguments should be names of installed applications, use
  453. ``AppCommand`` instead.
  454. """
  455. label = 'label'
  456. missing_args_message = "Enter at least one %s." % label
  457. def add_arguments(self, parser):
  458. parser.add_argument('args', metavar=self.label, nargs='+')
  459. def handle(self, *labels, **options):
  460. output = []
  461. for label in labels:
  462. label_output = self.handle_label(label, **options)
  463. if label_output:
  464. output.append(label_output)
  465. return '\n'.join(output)
  466. def handle_label(self, label, **options):
  467. """
  468. Perform the command's actions for ``label``, which will be the
  469. string as given on the command line.
  470. """
  471. raise NotImplementedError('subclasses of LabelCommand must provide a handle_label() method')