Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/core/management/base.py: 55%

252 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1""" 

2Base classes for writing management commands (named commands which can 

3be executed through ``django-admin`` or ``manage.py``). 

4""" 

5import argparse 

6import os 

7import sys 

8import warnings 

9from argparse import ArgumentParser, HelpFormatter 

10from io import TextIOBase 

11 

12import django 

13from django.core import checks 

14from django.core.exceptions import ImproperlyConfigured 

15from django.core.management.color import color_style, no_style 

16from django.db import DEFAULT_DB_ALIAS, connections 

17from django.utils.deprecation import RemovedInDjango41Warning 

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 ( 64 ↛ exit,   64 ↛ 672 missed branches: 1) line 64 didn't jump to the function exit, 2) line 64 didn't jump to line 67, because the condition on line 64 was never true

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 

77def handle_default_options(options): 

78 """ 

79 Include any default options that all commands should accept here 

80 so that ManagementUtility can handle them before searching for 

81 user commands. 

82 """ 

83 if options.settings: 83 ↛ 84line 83 didn't jump to line 84, because the condition on line 83 was never true

84 os.environ["DJANGO_SETTINGS_MODULE"] = options.settings 

85 if options.pythonpath: 85 ↛ 86line 85 didn't jump to line 86, because the condition on line 85 was never true

86 sys.path.insert(0, options.pythonpath) 

87 

88 

89def no_translations(handle_func): 

90 """Decorator that forces a command to run with translations deactivated.""" 

91 

92 def wrapped(*args, **kwargs): 

93 from django.utils import translation 

94 

95 saved_locale = translation.get_language() 

96 translation.deactivate_all() 

97 try: 

98 res = handle_func(*args, **kwargs) 

99 finally: 

100 if saved_locale is not None: 100 ↛ 102line 100 didn't jump to line 102, because the condition on line 100 was never false

101 translation.activate(saved_locale) 

102 return res 

103 

104 return wrapped 

105 

106 

107class DjangoHelpFormatter(HelpFormatter): 

108 """ 

109 Customized formatter so that command-specific arguments appear in the 

110 --help output before arguments common to all commands. 

111 """ 

112 

113 show_last = { 

114 "--version", 

115 "--verbosity", 

116 "--traceback", 

117 "--settings", 

118 "--pythonpath", 

119 "--no-color", 

120 "--force-color", 

121 "--skip-checks", 

122 } 

123 

124 def _reordered_actions(self, actions): 

125 return sorted( 

126 actions, key=lambda a: set(a.option_strings) & self.show_last != set() 

127 ) 

128 

129 def add_usage(self, usage, actions, *args, **kwargs): 

130 super().add_usage(usage, self._reordered_actions(actions), *args, **kwargs) 

131 

132 def add_arguments(self, actions): 

133 super().add_arguments(self._reordered_actions(actions)) 

134 

135 

136class OutputWrapper(TextIOBase): 

137 """ 

138 Wrapper around stdout/stderr 

139 """ 

140 

141 @property 

142 def style_func(self): 

143 return self._style_func 

144 

145 @style_func.setter 

146 def style_func(self, style_func): 

147 if style_func and self.isatty(): 

148 self._style_func = style_func 

149 else: 

150 self._style_func = lambda x: x 

151 

152 def __init__(self, out, ending="\n"): 

153 self._out = out 

154 self.style_func = None 

155 self.ending = ending 

156 

157 def __getattr__(self, name): 

158 return getattr(self._out, name) 

159 

160 def flush(self): 

161 if hasattr(self._out, "flush"): 161 ↛ exitline 161 didn't return from function 'flush', because the condition on line 161 was never false

162 self._out.flush() 

163 

164 def isatty(self): 

165 return hasattr(self._out, "isatty") and self._out.isatty() 

166 

167 def write(self, msg="", style_func=None, ending=None): 

168 ending = self.ending if ending is None else ending 

169 if ending and not msg.endswith(ending): 169 ↛ 171line 169 didn't jump to line 171, because the condition on line 169 was never false

170 msg += ending 

171 style_func = style_func or self.style_func 

172 self._out.write(style_func(msg)) 

173 

174 

175class BaseCommand: 

176 """ 

177 The base class from which all management commands ultimately 

178 derive. 

179 

180 Use this class if you want access to all of the mechanisms which 

181 parse the command-line arguments and work out what code to call in 

182 response; if you don't need to change any of that behavior, 

183 consider using one of the subclasses defined in this file. 

184 

185 If you are interested in overriding/customizing various aspects of 

186 the command-parsing and -execution behavior, the normal flow works 

187 as follows: 

188 

189 1. ``django-admin`` or ``manage.py`` loads the command class 

190 and calls its ``run_from_argv()`` method. 

191 

192 2. The ``run_from_argv()`` method calls ``create_parser()`` to get 

193 an ``ArgumentParser`` for the arguments, parses them, performs 

194 any environment changes requested by options like 

195 ``pythonpath``, and then calls the ``execute()`` method, 

196 passing the parsed arguments. 

197 

198 3. The ``execute()`` method attempts to carry out the command by 

199 calling the ``handle()`` method with the parsed arguments; any 

200 output produced by ``handle()`` will be printed to standard 

201 output and, if the command is intended to produce a block of 

202 SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``. 

203 

204 4. If ``handle()`` or ``execute()`` raised any exception (e.g. 

205 ``CommandError``), ``run_from_argv()`` will instead print an error 

206 message to ``stderr``. 

207 

208 Thus, the ``handle()`` method is typically the starting point for 

209 subclasses; many built-in commands and command types either place 

210 all of their logic in ``handle()``, or perform some additional 

211 parsing work in ``handle()`` and then delegate from it to more 

212 specialized methods as needed. 

213 

214 Several attributes affect behavior at various steps along the way: 

215 

216 ``help`` 

217 A short description of the command, which will be printed in 

218 help messages. 

219 

220 ``output_transaction`` 

221 A boolean indicating whether the command outputs SQL 

222 statements; if ``True``, the output will automatically be 

223 wrapped with ``BEGIN;`` and ``COMMIT;``. Default value is 

224 ``False``. 

225 

226 ``requires_migrations_checks`` 

227 A boolean; if ``True``, the command prints a warning if the set of 

228 migrations on disk don't match the migrations in the database. 

229 

230 ``requires_system_checks`` 

231 A list or tuple of tags, e.g. [Tags.staticfiles, Tags.models]. System 

232 checks registered in the chosen tags will be checked for errors prior 

233 to executing the command. The value '__all__' can be used to specify 

234 that all system checks should be performed. Default value is '__all__'. 

235 

236 To validate an individual application's models 

237 rather than all applications' models, call 

238 ``self.check(app_configs)`` from ``handle()``, where ``app_configs`` 

239 is the list of application's configuration provided by the 

240 app registry. 

241 

242 ``stealth_options`` 

243 A tuple of any options the command uses which aren't defined by the 

244 argument parser. 

245 """ 

246 

247 # Metadata about this command. 

248 help = "" 

249 

250 # Configuration shortcuts that alter various logic. 

251 _called_from_command_line = False 

252 output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;" 

253 requires_migrations_checks = False 

254 requires_system_checks = "__all__" 

255 # Arguments, common to all commands, which aren't defined by the argument 

256 # parser. 

257 base_stealth_options = ("stderr", "stdout") 

258 # Command-specific options not defined by the argument parser. 

259 stealth_options = () 

260 suppressed_base_arguments = set() 

261 

262 def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False): 

263 self.stdout = OutputWrapper(stdout or sys.stdout) 

264 self.stderr = OutputWrapper(stderr or sys.stderr) 

265 if no_color and force_color: 265 ↛ 266line 265 didn't jump to line 266, because the condition on line 265 was never true

266 raise CommandError("'no_color' and 'force_color' can't be used together.") 

267 if no_color: 267 ↛ 268line 267 didn't jump to line 268, because the condition on line 267 was never true

268 self.style = no_style() 

269 else: 

270 self.style = color_style(force_color) 

271 self.stderr.style_func = self.style.ERROR 

272 if self.requires_system_checks in [False, True]: 272 ↛ 273line 272 didn't jump to line 273, because the condition on line 272 was never true

273 warnings.warn( 

274 "Using a boolean value for requires_system_checks is " 

275 "deprecated. Use '__all__' instead of True, and [] (an empty " 

276 "list) instead of False.", 

277 RemovedInDjango41Warning, 

278 ) 

279 self.requires_system_checks = ( 

280 ALL_CHECKS if self.requires_system_checks else [] 

281 ) 

282 if ( 282 ↛ 286line 282 didn't jump to line 286

283 not isinstance(self.requires_system_checks, (list, tuple)) 

284 and self.requires_system_checks != ALL_CHECKS 

285 ): 

286 raise TypeError("requires_system_checks must be a list or tuple.") 

287 

288 def get_version(self): 

289 """ 

290 Return the Django version, which should be correct for all built-in 

291 Django commands. User-supplied commands can override this method to 

292 return their own version. 

293 """ 

294 return django.get_version() 

295 

296 def create_parser(self, prog_name, subcommand, **kwargs): 

297 """ 

298 Create and return the ``ArgumentParser`` which will be used to 

299 parse the arguments to this command. 

300 """ 

301 parser = CommandParser( 

302 prog="%s %s" % (os.path.basename(prog_name), subcommand), 

303 description=self.help or None, 

304 formatter_class=DjangoHelpFormatter, 

305 missing_args_message=getattr(self, "missing_args_message", None), 

306 called_from_command_line=getattr(self, "_called_from_command_line", None), 

307 **kwargs, 

308 ) 

309 self.add_base_argument( 

310 parser, 

311 "--version", 

312 action="version", 

313 version=self.get_version(), 

314 help="Show program's version number and exit.", 

315 ) 

316 self.add_base_argument( 

317 parser, 

318 "-v", 

319 "--verbosity", 

320 default=1, 

321 type=int, 

322 choices=[0, 1, 2, 3], 

323 help=( 

324 "Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, " 

325 "3=very verbose output" 

326 ), 

327 ) 

328 self.add_base_argument( 

329 parser, 

330 "--settings", 

331 help=( 

332 "The Python path to a settings module, e.g. " 

333 '"myproject.settings.main". If this isn\'t provided, the ' 

334 "DJANGO_SETTINGS_MODULE environment variable will be used." 

335 ), 

336 ) 

337 self.add_base_argument( 

338 parser, 

339 "--pythonpath", 

340 help=( 

341 "A directory to add to the Python path, e.g. " 

342 '"/home/djangoprojects/myproject".' 

343 ), 

344 ) 

345 self.add_base_argument( 

346 parser, 

347 "--traceback", 

348 action="store_true", 

349 help="Raise on CommandError exceptions.", 

350 ) 

351 self.add_base_argument( 

352 parser, 

353 "--no-color", 

354 action="store_true", 

355 help="Don't colorize the command output.", 

356 ) 

357 self.add_base_argument( 

358 parser, 

359 "--force-color", 

360 action="store_true", 

361 help="Force colorization of the command output.", 

362 ) 

363 if self.requires_system_checks: 363 ↛ 364line 363 didn't jump to line 364, because the condition on line 363 was never true

364 parser.add_argument( 

365 "--skip-checks", 

366 action="store_true", 

367 help="Skip system checks.", 

368 ) 

369 self.add_arguments(parser) 

370 return parser 

371 

372 def add_arguments(self, parser): 

373 """ 

374 Entry point for subclassed commands to add custom arguments. 

375 """ 

376 pass 

377 

378 def add_base_argument(self, parser, *args, **kwargs): 

379 """ 

380 Call the parser's add_argument() method, suppressing the help text 

381 according to BaseCommand.suppressed_base_arguments. 

382 """ 

383 for arg in args: 

384 if arg in self.suppressed_base_arguments: 384 ↛ 385line 384 didn't jump to line 385, because the condition on line 384 was never true

385 kwargs["help"] = argparse.SUPPRESS 

386 break 

387 parser.add_argument(*args, **kwargs) 

388 

389 def print_help(self, prog_name, subcommand): 

390 """ 

391 Print the help message for this command, derived from 

392 ``self.usage()``. 

393 """ 

394 parser = self.create_parser(prog_name, subcommand) 

395 parser.print_help() 

396 

397 def run_from_argv(self, argv): 

398 """ 

399 Set up any environment changes requested (e.g., Python path 

400 and Django settings), then run this command. If the 

401 command raises a ``CommandError``, intercept it and print it sensibly 

402 to stderr. If the ``--traceback`` option is present or the raised 

403 ``Exception`` is not ``CommandError``, raise it. 

404 """ 

405 self._called_from_command_line = True 

406 parser = self.create_parser(argv[0], argv[1]) 

407 

408 options = parser.parse_args(argv[2:]) 

409 cmd_options = vars(options) 

410 # Move positional args out of options to mimic legacy optparse 

411 args = cmd_options.pop("args", ()) 

412 handle_default_options(options) 

413 try: 

414 self.execute(*args, **cmd_options) 

415 except CommandError as e: 

416 if options.traceback: 

417 raise 

418 

419 # SystemCheckError takes care of its own formatting. 

420 if isinstance(e, SystemCheckError): 

421 self.stderr.write(str(e), lambda x: x) 

422 else: 

423 self.stderr.write("%s: %s" % (e.__class__.__name__, e)) 

424 sys.exit(e.returncode) 

425 finally: 

426 try: 

427 connections.close_all() 

428 except ImproperlyConfigured: 

429 # Ignore if connections aren't setup at this point (e.g. no 

430 # configured settings). 

431 pass 

432 

433 def execute(self, *args, **options): 

434 """ 

435 Try to execute this command, performing system checks if needed (as 

436 controlled by the ``requires_system_checks`` attribute, except if 

437 force-skipped). 

438 """ 

439 if options["force_color"] and options["no_color"]: 439 ↛ 440line 439 didn't jump to line 440, because the condition on line 439 was never true

440 raise CommandError( 

441 "The --no-color and --force-color options can't be used together." 

442 ) 

443 if options["force_color"]: 443 ↛ 444line 443 didn't jump to line 444, because the condition on line 443 was never true

444 self.style = color_style(force_color=True) 

445 elif options["no_color"]: 445 ↛ 446line 445 didn't jump to line 446, because the condition on line 445 was never true

446 self.style = no_style() 

447 self.stderr.style_func = None 

448 if options.get("stdout"): 448 ↛ 449line 448 didn't jump to line 449, because the condition on line 448 was never true

449 self.stdout = OutputWrapper(options["stdout"]) 

450 if options.get("stderr"): 450 ↛ 451line 450 didn't jump to line 451, because the condition on line 450 was never true

451 self.stderr = OutputWrapper(options["stderr"]) 

452 

453 if self.requires_system_checks and not options["skip_checks"]: 453 ↛ 454line 453 didn't jump to line 454, because the condition on line 453 was never true

454 if self.requires_system_checks == ALL_CHECKS: 

455 self.check() 

456 else: 

457 self.check(tags=self.requires_system_checks) 

458 if self.requires_migrations_checks: 458 ↛ 459line 458 didn't jump to line 459, because the condition on line 458 was never true

459 self.check_migrations() 

460 output = self.handle(*args, **options) 

461 if output: 461 ↛ 462line 461 didn't jump to line 462, because the condition on line 461 was never true

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 check( 

473 self, 

474 app_configs=None, 

475 tags=None, 

476 display_num_errors=False, 

477 include_deployment_checks=False, 

478 fail_level=checks.ERROR, 

479 databases=None, 

480 ): 

481 """ 

482 Use the system check framework to validate entire Django project. 

483 Raise CommandError for any serious message (error or critical errors). 

484 If there are only light messages (like warnings), print them to stderr 

485 and don't raise an exception. 

486 """ 

487 all_issues = checks.run_checks( 

488 app_configs=app_configs, 

489 tags=tags, 

490 include_deployment_checks=include_deployment_checks, 

491 databases=databases, 

492 ) 

493 

494 header, body, footer = "", "", "" 

495 visible_issue_count = 0 # excludes silenced warnings 

496 

497 if all_issues: 497 ↛ 498line 497 didn't jump to line 498, because the condition on line 497 was never true

498 debugs = [ 

499 e for e in all_issues if e.level < checks.INFO and not e.is_silenced() 

500 ] 

501 infos = [ 

502 e 

503 for e in all_issues 

504 if checks.INFO <= e.level < checks.WARNING and not e.is_silenced() 

505 ] 

506 warnings = [ 

507 e 

508 for e in all_issues 

509 if checks.WARNING <= e.level < checks.ERROR and not e.is_silenced() 

510 ] 

511 errors = [ 

512 e 

513 for e in all_issues 

514 if checks.ERROR <= e.level < checks.CRITICAL and not e.is_silenced() 

515 ] 

516 criticals = [ 

517 e 

518 for e in all_issues 

519 if checks.CRITICAL <= e.level and not e.is_silenced() 

520 ] 

521 sorted_issues = [ 

522 (criticals, "CRITICALS"), 

523 (errors, "ERRORS"), 

524 (warnings, "WARNINGS"), 

525 (infos, "INFOS"), 

526 (debugs, "DEBUGS"), 

527 ] 

528 

529 for issues, group_name in sorted_issues: 

530 if issues: 

531 visible_issue_count += len(issues) 

532 formatted = ( 

533 self.style.ERROR(str(e)) 

534 if e.is_serious() 

535 else self.style.WARNING(str(e)) 

536 for e in issues 

537 ) 

538 formatted = "\n".join(sorted(formatted)) 

539 body += "\n%s:\n%s\n" % (group_name, formatted) 

540 

541 if visible_issue_count: 541 ↛ 542line 541 didn't jump to line 542, because the condition on line 541 was never true

542 header = "System check identified some issues:\n" 

543 

544 if display_num_errors: 544 ↛ 556line 544 didn't jump to line 556, because the condition on line 544 was never false

545 if visible_issue_count: 545 ↛ 546line 545 didn't jump to line 546, because the condition on line 545 was never true

546 footer += "\n" 

547 footer += "System check identified %s (%s silenced)." % ( 

548 "no issues" 

549 if visible_issue_count == 0 

550 else "1 issue" 

551 if visible_issue_count == 1 

552 else "%s issues" % visible_issue_count, 

553 len(all_issues) - visible_issue_count, 

554 ) 

555 

556 if any(e.is_serious(fail_level) and not e.is_silenced() for e in all_issues): 556 ↛ 557line 556 didn't jump to line 557, because the condition on line 556 was never true

557 msg = self.style.ERROR("SystemCheckError: %s" % header) + body + footer 

558 raise SystemCheckError(msg) 

559 else: 

560 msg = header + body + footer 

561 

562 if msg: 562 ↛ exitline 562 didn't return from function 'check', because the condition on line 562 was never false

563 if visible_issue_count: 563 ↛ 564line 563 didn't jump to line 564, because the condition on line 563 was never true

564 self.stderr.write(msg, lambda x: x) 

565 else: 

566 self.stdout.write(msg) 

567 

568 def check_migrations(self): 

569 """ 

570 Print a warning if the set of migrations on disk don't match the 

571 migrations in the database. 

572 """ 

573 from django.db.migrations.executor import MigrationExecutor 

574 

575 try: 

576 executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS]) 

577 except ImproperlyConfigured: 

578 # No databases are configured (or the dummy one) 

579 return 

580 

581 plan = executor.migration_plan(executor.loader.graph.leaf_nodes()) 

582 if plan: 

583 apps_waiting_migration = sorted( 

584 {migration.app_label for migration, backwards in plan} 

585 ) 

586 self.stdout.write( 

587 self.style.NOTICE( 

588 "\nYou have %(unapplied_migration_count)s unapplied migration(s). " 

589 "Your project may not work properly until you apply the " 

590 "migrations for app(s): %(apps_waiting_migration)s." 

591 % { 

592 "unapplied_migration_count": len(plan), 

593 "apps_waiting_migration": ", ".join(apps_waiting_migration), 

594 } 

595 ) 

596 ) 

597 self.stdout.write( 

598 self.style.NOTICE("Run 'python manage.py migrate' to apply them.") 

599 ) 

600 

601 def handle(self, *args, **options): 

602 """ 

603 The actual logic of the command. Subclasses must implement 

604 this method. 

605 """ 

606 raise NotImplementedError( 

607 "subclasses of BaseCommand must provide a handle() method" 

608 ) 

609 

610 

611class AppCommand(BaseCommand): 

612 """ 

613 A management command which takes one or more installed application labels 

614 as arguments, and does something with each of them. 

615 

616 Rather than implementing ``handle()``, subclasses must implement 

617 ``handle_app_config()``, which will be called once for each application. 

618 """ 

619 

620 missing_args_message = "Enter at least one application label." 

621 

622 def add_arguments(self, parser): 

623 parser.add_argument( 

624 "args", 

625 metavar="app_label", 

626 nargs="+", 

627 help="One or more application label.", 

628 ) 

629 

630 def handle(self, *app_labels, **options): 

631 from django.apps import apps 

632 

633 try: 

634 app_configs = [apps.get_app_config(app_label) for app_label in app_labels] 

635 except (LookupError, ImportError) as e: 

636 raise CommandError( 

637 "%s. Are you sure your INSTALLED_APPS setting is correct?" % e 

638 ) 

639 output = [] 

640 for app_config in app_configs: 

641 app_output = self.handle_app_config(app_config, **options) 

642 if app_output: 

643 output.append(app_output) 

644 return "\n".join(output) 

645 

646 def handle_app_config(self, app_config, **options): 

647 """ 

648 Perform the command's actions for app_config, an AppConfig instance 

649 corresponding to an application label given on the command line. 

650 """ 

651 raise NotImplementedError( 

652 "Subclasses of AppCommand must provide a handle_app_config() method." 

653 ) 

654 

655 

656class LabelCommand(BaseCommand): 

657 """ 

658 A management command which takes one or more arbitrary arguments 

659 (labels) on the command line, and does something with each of 

660 them. 

661 

662 Rather than implementing ``handle()``, subclasses must implement 

663 ``handle_label()``, which will be called once for each label. 

664 

665 If the arguments should be names of installed applications, use 

666 ``AppCommand`` instead. 

667 """ 

668 

669 label = "label" 

670 missing_args_message = "Enter at least one %s." % label 

671 

672 def add_arguments(self, parser): 

673 parser.add_argument("args", metavar=self.label, nargs="+") 

674 

675 def handle(self, *labels, **options): 

676 output = [] 

677 for label in labels: 

678 label_output = self.handle_label(label, **options) 

679 if label_output: 

680 output.append(label_output) 

681 return "\n".join(output) 

682 

683 def handle_label(self, label, **options): 

684 """ 

685 Perform the command's actions for ``label``, which will be the 

686 string as given on the command line. 

687 """ 

688 raise NotImplementedError( 

689 "subclasses of LabelCommand must provide a handle_label() method" 

690 )