6
6
Tutorial: Inheritance & Plugins
7
7
===============================
8
8
9
- You may need to change the behavior of an
10
- `upstream command <https://en.wikipedia.org/wiki/Upstream_(software_development) >`_ or wish
11
- you could add an additional subcommand or group to it. django-typer _ offers two patterns for
12
- changing or extending the behavior of commands. :class: `~django_typer.management.TyperCommand `
13
- classes :ref: `support inheritance <inheritance >`, even multiple inheritance. This can be a way to
14
- override or add additional commands to a command implemented elsewhere. You can then use Django's
15
- built in command override precedence (INSTALLED_APPS) to ensure your command is used instead of the
16
- upstream command or give it a different name if you would like the upstream command to still be
17
- available. The :ref: `plugin pattern <plugin >` allows commands and groups to be added or overridden
18
- directly on upstream commands without inheritance. This mechanism is useful when you might expect
19
- other apps to also modify the original command. Conflicts are resolved in INSTALLED_APPS order.
9
+ Adding to, or altering the behavior of, commands from upstream Django _ apps is a common use case.
10
+ Doing so allows you to keep your CLI _ stable while adding additional behaviors through your site's
11
+ configuration settings files. There are three main extension patterns you may wish to employ:
12
+
13
+ 1. Override the behavior of a command in an upstream app.
14
+ 2. Add additional subcommands or groups to a command in an upstream app.
15
+ 3. Hook implementations of custom logic into upstream command extension points.
16
+ (`Inversion of Control <https://en.wikipedia.org/wiki/Inversion_of_control >`_)
17
+
18
+ The django-typer _ plugin mechanism supports all three of these use cases in a way that respects
19
+ the precedence order of apps in the ``INSTALLED_APPS `` setting. In this tutorial we walk through
20
+ an example of each using a :ref: `generic backup command <generic_backup >`. First we'll see how we
21
+ might :ref: `use inheritance (1) <inheritance >` to override and change the behavior of a
22
+ subcommand. Then we'll see how we can :ref: `add subcommands (2) <plugin >` to an upstream command
23
+ using plugins. Finally we'll use pluggy _ to implement a hook system that allows us to
24
+ :ref: `add custom logic (3) <hooks >` to an upstream command.
25
+
26
+ .. _generic_backup :
27
+
28
+ A Generic Backup Command
29
+ -------------------------
20
30
21
31
Consider the task of backing up a Django website. State is stored in the database, in media files
22
32
on disk, potentially in other files, and also in the software stack running on the server. If we
@@ -68,8 +78,8 @@ Inheritance
68
78
-----------
69
79
70
80
The first option we have is simple inheritance. Lets say the base command is defined in
71
- an app called backup. Now say we have another app that adds functionality that uses media
72
- files to our site. This means we'll want to add a media backup routine to the backup command.
81
+ an app called backup. Now say we have another app that uses media files. This means we'll
82
+ want to add a media backup routine to the backup command.
73
83
74
84
.. note ::
75
85
@@ -162,17 +172,14 @@ backup batch:
162
172
[.............................................]
163
173
Backing up ./media to ./media.tar.gz
164
174
165
- .. warning ::
166
-
167
- Inheritance is not supported from typical Django commands that used argparse to define their
168
- interface.
169
175
176
+ .. _inheritance_rationale :
170
177
171
178
When Does Inheritance Make Sense?
172
179
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
173
180
174
- Inheritance is a good choice when you want to tweak the behavior of a specific command and do not
175
- expect other apps to also modify the same command. It's also a good choice when you want to offer
181
+ Inheritance is a good choice when you want to tweak the behavior of a specific command and ** do not
182
+ expect other apps to also modify the same command ** . It's also a good choice when you want to offer
176
183
a different flavor of a command under a different name.
177
184
178
185
What if other apps want to alter the same command and we don't know about them, but they may end up
@@ -181,7 +188,7 @@ installed along with our app? This is where the plugin pattern will serve us bet
181
188
182
189
.. _plugin :
183
190
184
- Plugins
191
+ CLI Plugins
185
192
-----------
186
193
187
194
**The plugin pattern allows us to add or override commands and groups on an upstream command
@@ -194,10 +201,10 @@ than ``commands``. Let us suppose we are developing a site that uses the backup
194
201
upstream and we've implemented most of our custom site functionality in a new app called my_app.
195
202
Because we're now mostly working at the level of our particular site we may want to add more custom
196
203
backup logic. For instance, lets say we know our site will always run on sqlite and we prefer
197
- to just copy the file to backup our database. Lets also pretend that it is useful for us to backup
198
- the python stack (e.g. requirements.txt) running on our server. To do that we can use the
199
- plugin pattern to add our environment backup routine and override the database routine from
200
- the upstream backup app. Our app tree now might look like this:
204
+ to just copy the file to backup our database. It is also useful for us to capture the python stack
205
+ (e.g. requirements.txt) running on our server. To do that we can use the plugin pattern to add our
206
+ environment backup routine and override the database routine from the upstream backup app. Our app
207
+ tree now might look like this:
201
208
202
209
.. code-block :: text
203
210
@@ -230,10 +237,9 @@ the upstream backup app. Our app tree now might look like this:
230
237
└── backup.py
231
238
232
239
233
- Note that we've added an ``plugins `` directory to the management directory of the media and
234
- my_app apps. This is where we'll place our extension commands. There is an additional step we must
235
- take. In the ``apps.py `` file of the media and my_app apps we must register our plugins like
236
- this:
240
+ Note that we've added a ``plugins `` directory to the management directory of the ``media `` and
241
+ ``my_app `` apps. This is where we'll place our command extensions. We must register our plugins
242
+ directory in the ``apps.py `` file of the media and my_app apps like this:
237
243
238
244
.. code-block :: python
239
245
@@ -253,14 +259,14 @@ this:
253
259
Because we explicitly register our plugins we can call the package whatever we want.
254
260
django-typer does not require it to be named ``plugins ``. It is also important to
255
261
do this inside ready() because conflicts are resolved in the order in which the extension
256
- modules are registered and ready() methods are called in INSTALLED_APPS order.
262
+ modules are registered and ready() methods are called in `` INSTALLED_APPS `` order.
257
263
258
- For plugins to work, we'll need to re-mplement media from above as a composed extension
259
- and that would look like this:
264
+ For plugins to work, we'll need to re-implement media from above as a composed extension
265
+ like this:
260
266
261
267
.. tabs ::
262
268
263
- .. tab :: django-typer
269
+ .. tab :: Django-style
264
270
265
271
.. literalinclude :: ../../tests/apps/examples/plugins/media2/management/plugins/backup.py
266
272
:language: python
@@ -425,8 +431,10 @@ You may even override the initializer of a predefined group:
425
431
(i.e. ``Command.grp1.grp2.grp3.cmd ``), if there is only one cmd you can simply write
426
432
``Command.cmd ``. However, using the strict hierarchy will be robust to future changes.
427
433
428
- When Do Plugins Make Sense?
429
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
434
+ .. _cli_plugin_rationale :
435
+
436
+ When Do CLI Plugins Make Sense?
437
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
430
438
431
439
Plugins can be used to group like behavior together under a common root command. This can be
432
440
thought of as a way to namespace CLI tools or easily share significant code between tools that have
@@ -441,3 +449,124 @@ Plugins can be a good way to organize commands in a code base that follows this
441
449
also allows for deployments that install a subset of those apps and is therefore a good way to
442
450
organize commands in code bases that serve as a framework for a particular kind of site or that
443
451
support selecting the features to install by the inclusion or exclusion of specific apps.
452
+
453
+
454
+ .. _hooks :
455
+
456
+ Logic Plugins
457
+ -------------
458
+
459
+ `Inversion of Control (IoC) <https://en.wikipedia.org/wiki/Inversion_of_control >`_ is a design
460
+ pattern that allows you to inject custom logic into a framework or library. The framework defines
461
+ the general execution flow with extension points along the way that downstream applications can
462
+ provide the implementations for. Django uses IoC all over the place. Extension points are often
463
+ called ``hooks ``. **You may use a third party library to manage hooks or implement your own
464
+ mechanism but you will always need to register hook implementations. The same plugin mechanism we
465
+ used in the ** :ref: `last section <plugin >` **provides a natural place to do this. **
466
+
467
+ Some Django _ apps may keep state in files in places on the filesystem unknown to other parts of
468
+ your code base. In this section we'll use pluggy _ to define a hook for other apps to implement to
469
+ backup their own files. Let's:
470
+
471
+ 1. Create a new app ``backup_files `` and inherit from our the extended media backup command we
472
+ created in the :ref: `inheritance section <inheritance >`.
473
+ 2. Define a pluggy _ interface for backing up arbitrary files
474
+ 3. Add a ``files `` command to our backup command that will call all registered
475
+ hooks to backup their own files.
476
+
477
+ .. tabs ::
478
+
479
+ .. tab :: Django-style
480
+
481
+ .. literalinclude :: ../../tests/apps/examples/plugins/backup_files/management/commands/backup.py
482
+ :language: python
483
+ :caption: backup_files/management/commands/backup.py
484
+ :linenos:
485
+ :replace:
486
+ tests.apps.examples.plugins.media1: media
487
+
488
+ .. tab :: Typer-style
489
+
490
+ .. literalinclude :: ../../tests/apps/examples/plugins/backup_files/management/commands/backup_typer.py
491
+ :language: python
492
+ :caption: backup_files/management/commands/backup.py
493
+ :linenos:
494
+ :replace:
495
+ tests.apps.examples.plugins.media1: media
496
+
497
+ Now lets define two new apps, files1 and files2 that will provide and register implementations of
498
+ the backup_files hook:
499
+
500
+ .. tabs ::
501
+
502
+ .. tab :: Django-style
503
+
504
+ .. literalinclude :: ../../tests/apps/examples/plugins/files1/management/plugins/backup.py
505
+ :language: python
506
+ :caption: files1/management/plugins/backup.py
507
+ :linenos:
508
+ :replace:
509
+ tests.apps.examples.plugins.backup_files: backup_files
510
+
511
+ .. tab :: Typer-style
512
+
513
+ .. literalinclude :: ../../tests/apps/examples/plugins/files1/management/plugins/backup_typer.py
514
+ :language: python
515
+ :caption: files1/management/plugins/backup.py
516
+ :linenos:
517
+ :replace:
518
+ tests.apps.examples.plugins.backup_files: backup_files
519
+
520
+ .. tabs ::
521
+
522
+ .. tab :: Django-style
523
+
524
+ .. literalinclude :: ../../tests/apps/examples/plugins/files2/management/plugins/backup.py
525
+ :language: python
526
+ :caption: files2/management/plugins/backup.py
527
+ :linenos:
528
+ :replace:
529
+ tests.apps.examples.plugins.backup_files: backup_files
530
+
531
+ .. tab :: Typer-style
532
+
533
+ .. literalinclude :: ../../tests/apps/examples/plugins/files2/management/plugins/backup_typer.py
534
+ :language: python
535
+ :caption: files2/management/plugins/backup.py
536
+ :linenos:
537
+ :replace:
538
+ tests.apps.examples.plugins.backup_files: backup_files
539
+
540
+ Both ``files1 `` and ``files2 `` will need to register their plugin packages in their ``apps.py ``
541
+ file:
542
+
543
+ .. literalinclude :: ../../tests/apps/examples/plugins/files1/apps.py
544
+ :language: python
545
+ :caption: files1/apps.py
546
+ :linenos:
547
+ :replace:
548
+ tests.apps.examples.plugins.files1: files1
549
+
550
+ Now when we run we see:
551
+
552
+ .. code-block :: bash
553
+
554
+ $> python manage.py backup
555
+ Backing up database [default] to: ./default.json
556
+ [.............................................]
557
+ Backing up ./media to ./media.tar.gz
558
+ Backed up files to ./files2.zip
559
+ Backed up files to ./files1.tar.gz
560
+
561
+
562
+ When Do Logic Plugins Make Sense?
563
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
564
+
565
+ :ref: `CLI plugins make sense <cli_plugin_rationale >` when you want to add additional commands or
566
+ under a common namespace or to override the entire behavior of a command. Logical plugins make
567
+ more sense in the weeds of a particular subroutine. Our example above has the following qualities
568
+ which makes it a good candidate:
569
+
570
+ 1. The logic makes sense under a common root name (e.g. ``./manage.py backup files ``).
571
+ 2. Multiple apps may need to execute their own version of the logic to complete the operation.
572
+ 3. The logic is amenable to a common interface that all plugins can implement.
0 commit comments