Skip to content

Commit a5a48ae

Browse files
committed
docs for finalize #140
1 parent fadc53f commit a5a48ae

12 files changed

+382
-13
lines changed

doc/source/changelog.rst

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Change Log
33
==========
44

5+
v3.0.0 (202X-XX-XX)
6+
===================
7+
8+
* Fixed `supressed_base_arguments are still present in the Context <https://github.com/django-commons/django-typer/issues/143>`_
9+
* Implemented `Add a @finalize decorator for functions to collect/operate on subroutine results. <https://github.com/django-commons/django-typer/issues/140>`_
10+
511
v2.4.0 (2024-11-07)
612
===================
713

doc/source/howto.rst

+70-5
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,79 @@ decorator. This is like defining a group at the command root and is an extension
263263
assert not command.subcommand2()
264264
265265
266-
.. _howto_finalizer:
266+
.. _howto_finalizers:
267267

268-
Collect and Finalize Results
269-
----------------------------
268+
Collect Results with @finalize
269+
------------------------------
270+
271+
Typer_ and Click_ have a ``results_callback`` mechanism on ``MultiCommands`` that allow a function
272+
hook to be registered to operate on the results of subroutines before the command exits. You may
273+
use this same ``results_callback`` mechanism directly through the Typer_ interface, but
274+
django-typer_ offers a more convenient class-aware way to do this with the
275+
:func:`~django_typer.management.finalize` decorator.
276+
277+
For example lets say we have two subcommands that return strings, we could turn them into a csv
278+
string by registering a callback with :func:`~django_typer.management.finalize`:
279+
280+
.. tabs::
281+
282+
.. tab:: Django-style
283+
284+
.. literalinclude:: ../../tests/apps/howto/management/commands/finalize.py
285+
286+
.. tab:: Typer-style
287+
288+
.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_typer.py
289+
290+
291+
.. tab:: Typer-style w/finalize
292+
293+
.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_typer_ext.py
294+
295+
296+
.. code-block:: console
297+
298+
$> ./manage.py finalizer cmd1 cmd1 cmd2
299+
result1, result2, result3
300+
301+
.. tip::
302+
303+
@finalize() wrapped callbacks will be passed the CLI parameters on the current context
304+
if the function signature accepts them. While convenient, we recommend using command state to
305+
track these parameters instead. This will be more amenable to direct invocations of command
306+
object functions.
307+
308+
Use @finalize on groups
309+
~~~~~~~~~~~~~~~~~~~~~~~
310+
311+
Finalizers are hierarchical. The :func:`~django_typer.management.finalize` decorator is available
312+
for use on subgroups. When used on a group, the callback will be invoked after the group's
313+
subcommands have been executed and the return value of the finalizer will be passed up to any
314+
finalizers at higher levels in the command hierarchy.
315+
316+
.. tabs::
317+
318+
.. tab:: Django-style
319+
320+
.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_group.py
321+
322+
.. tab:: Typer-style
323+
324+
.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_group_typer.py
325+
326+
.. tab:: Typer-style w/finalize
327+
328+
.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_group_typer_ext.py
329+
330+
.. code-block:: console
331+
332+
$> ./manage.py finalizer cmd1 cmd1 cmd2 grp cmd4 cmd3
333+
result1, result2, result3, RESULT4, RESULT3
334+
335+
.. tip::
270336

271-
.. TODO::
337+
Finalizers can be overridden just like groups and initializers using the :ref:`plugin pattern. <plugins>`
272338

273-
This section is not yet implemented.
274339

275340
Call Commands from Code
276341
-----------------------

doc/source/reference.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ django_typer
1919
:show-inheritance:
2020

2121
.. autoclass:: django_typer.management.Typer
22-
:members: callback, initialize, command, group, add_typer
22+
:members: callback, initialize, finalize, command, group, add_typer
2323

2424
.. autoclass:: django_typer.management.TyperCommand
25-
:members: initialize, callback, command, group, echo, secho, print_help, get_subcommand
25+
:members: initialize, callback, finalize, command, group, echo, secho, print_help, get_subcommand
2626

2727
.. autoclass:: django_typer.management.CommandNode
2828
:members: name, click_command, context, children, get_command, print_help
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import typing as t
2+
from django_typer.management import TyperCommand, command, finalize
3+
4+
5+
# chain=True allows multiple subroutines to be called from the command line
6+
class Command(TyperCommand, chain=True):
7+
@finalize()
8+
def to_csv(self, results: t.List[str]):
9+
return ", ".join(results)
10+
11+
@command()
12+
def cmd1(self):
13+
return "result1"
14+
15+
@command()
16+
def cmd2(self):
17+
return "result2"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import typing as t
2+
from django_typer.management import TyperCommand, command, finalize, group
3+
4+
5+
class Command(TyperCommand, chain=True):
6+
"""
7+
Show that finalizers are hierarchical and results are collected and
8+
passed to the finalizer of the parent group if one exists.
9+
"""
10+
11+
@finalize()
12+
def to_csv(self, results: t.List[str]):
13+
return ", ".join(results)
14+
15+
@command()
16+
def cmd1(self):
17+
return "result1"
18+
19+
@command()
20+
def cmd2(self):
21+
return "result2"
22+
23+
@group(chain=True)
24+
def grp(self):
25+
return "grp"
26+
27+
@grp.finalize()
28+
def to_upper_csv(self, results):
29+
return ", ".join([result.upper() for result in results])
30+
31+
@grp.command()
32+
def cmd3(self):
33+
return "result3"
34+
35+
@grp.command()
36+
def cmd4(self):
37+
return "result4"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from django_typer.management import Typer
2+
3+
# use native Typer interface to achieve the same result
4+
5+
def to_csv(results, **_):
6+
return ", ".join(results)
7+
8+
9+
def to_upper_csv(results, **_):
10+
return ", ".join([result.upper() for result in results])
11+
12+
13+
app = Typer(result_callback=to_csv, chain=True)
14+
15+
grp = Typer(result_callback=to_upper_csv, chain=True)
16+
app.add_typer(grp, name="grp")
17+
18+
19+
@app.command()
20+
def cmd1():
21+
return "result1"
22+
23+
24+
@app.command()
25+
def cmd2():
26+
return "result2"
27+
28+
29+
@grp.command()
30+
def cmd3():
31+
return "result3"
32+
33+
34+
@grp.command()
35+
def cmd4():
36+
return "result4"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django_typer.management import Typer
2+
3+
# Use extensions to the typer interface to improve clarity
4+
5+
app = Typer(chain=True)
6+
7+
8+
@app.finalize()
9+
def to_csv(results):
10+
return ", ".join(results)
11+
12+
13+
@app.group(chain=True)
14+
def grp():
15+
pass
16+
17+
18+
@grp.finalize()
19+
def to_upper_csv(results):
20+
return ", ".join([result.upper() for result in results])
21+
22+
23+
@app.command()
24+
def cmd1():
25+
return "result1"
26+
27+
28+
@app.command()
29+
def cmd2():
30+
return "result2"
31+
32+
33+
@grp.command()
34+
def cmd3():
35+
return "result3"
36+
37+
38+
@grp.command()
39+
def cmd4():
40+
return "result4"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from django_typer.management import Typer
2+
3+
4+
def to_csv(results, **_):
5+
# result_callback is passed the CLI parameters on the current context
6+
# if we are uninterested in them, we can use the **_ syntax to ignore them
7+
return ", ".join(results)
8+
9+
10+
app = Typer(result_callback=to_csv, chain=True)
11+
12+
13+
@app.command()
14+
def cmd1():
15+
return "result1"
16+
17+
18+
@app.command()
19+
def cmd2():
20+
return "result2"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django_typer.management import Typer
2+
3+
# alternatively we can use the finalize nomenclature of the TyperCommand
4+
# interface - this is a non-standard Typer extension
5+
6+
app = Typer(chain=True)
7+
8+
9+
# The Typer interface is extended with the finalize decorator
10+
@app.finalize()
11+
def to_csv(results):
12+
return ", ".join(results)
13+
14+
15+
@app.command()
16+
def cmd1():
17+
return "result1"
18+
19+
20+
@app.command()
21+
def cmd2():
22+
return "result2"

tests/apps/test_app/management/extensions/finalize_subgroups_inherit.py

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ def grp1_collect(self, result, **kwargs):
1010
return f"grp1_collect: {self.grp1_final(result, **kwargs)}"
1111

1212

13+
@FinalizeSubgroupsInherit.grp2.finalize()
14+
def grp2_collect(result, **kwargs):
15+
return f"grp2_collect: {FinalizeSubgroupsInherit.grp2_final(result, **kwargs)}"
16+
17+
1318
@FinalizeSubgroupsInherit.grp1.command()
1419
def cmd5(self):
1520
return "cmd5"

tests/test_finalize.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -378,15 +378,15 @@ def test_finalize_subgroups_inherit_run(self):
378378
stdout, _, _ = run_command("finalize_subgroups_inherit", "grp2", "cmd4", "cmd3")
379379
self.assertEqual(
380380
stdout.strip(),
381-
"root_final: grp2_final: ['cmd4', 'cmd3'] | g2_opt=True | init_opt=False",
381+
"root_final: grp2_collect: grp2_final: ['cmd4', 'cmd3'] | g2_opt=True | init_opt=False",
382382
)
383383

384384
stdout, _, _ = run_command(
385385
"finalize_subgroups_inherit", "--init-opt", "grp2", "--no-g2-opt", "cmd3"
386386
)
387387
self.assertEqual(
388388
stdout.strip(),
389-
"root_final: grp2_final: ['cmd3'] | g2_opt=False | init_opt=True",
389+
"root_final: grp2_collect: grp2_final: ['cmd3'] | g2_opt=False | init_opt=True",
390390
)
391391

392392
stdout, _, _ = run_command(
@@ -420,7 +420,7 @@ def test_finalize_subgroups_inherit_call(self):
420420
)
421421
self.assertEqual(
422422
out.getvalue().strip(),
423-
"root_final: grp2_final: ['cmd4', 'cmd3'] | g2_opt=True | init_opt=False",
423+
"root_final: grp2_collect: grp2_final: ['cmd4', 'cmd3'] | g2_opt=True | init_opt=False",
424424
)
425425

426426
out = StringIO()
@@ -435,7 +435,7 @@ def test_finalize_subgroups_inherit_call(self):
435435
)
436436
self.assertEqual(
437437
out.getvalue().strip(),
438-
"root_final: grp2_final: ['cmd3'] | g2_opt=False | init_opt=True",
438+
"root_final: grp2_collect: grp2_final: ['cmd3'] | g2_opt=False | init_opt=True",
439439
)
440440

441441
out = StringIO()
@@ -521,5 +521,5 @@ def test_finalize_subgroups_inherit_obj(self):
521521
],
522522
init_opt=False,
523523
).strip(),
524-
"root_final: [\"grp2_final: ['cmd3', 'cmd4'] | g2_opt=False\"] | init_opt=False",
524+
"root_final: [\"grp2_collect: grp2_final: ['cmd3', 'cmd4'] | g2_opt=False\"] | init_opt=False",
525525
)

0 commit comments

Comments
 (0)