plain 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl

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