Skip to content

Commit

Permalink
docs for finalize #140
Browse files Browse the repository at this point in the history
  • Loading branch information
bckohan committed Nov 21, 2024
1 parent fadc53f commit a5a48ae
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 13 deletions.
6 changes: 6 additions & 0 deletions doc/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Change Log
==========

v3.0.0 (202X-XX-XX)
===================

* Fixed `supressed_base_arguments are still present in the Context <https://github.com/django-commons/django-typer/issues/143>`_
* Implemented `Add a @finalize decorator for functions to collect/operate on subroutine results. <https://github.com/django-commons/django-typer/issues/140>`_

v2.4.0 (2024-11-07)
===================

Expand Down
75 changes: 70 additions & 5 deletions doc/source/howto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,79 @@ decorator. This is like defining a group at the command root and is an extension
assert not command.subcommand2()
.. _howto_finalizer:
.. _howto_finalizers:

Collect and Finalize Results
----------------------------
Collect Results with @finalize
------------------------------

Typer_ and Click_ have a ``results_callback`` mechanism on ``MultiCommands`` that allow a function
hook to be registered to operate on the results of subroutines before the command exits. You may
use this same ``results_callback`` mechanism directly through the Typer_ interface, but
django-typer_ offers a more convenient class-aware way to do this with the
:func:`~django_typer.management.finalize` decorator.

For example lets say we have two subcommands that return strings, we could turn them into a csv
string by registering a callback with :func:`~django_typer.management.finalize`:

.. tabs::

.. tab:: Django-style

.. literalinclude:: ../../tests/apps/howto/management/commands/finalize.py

.. tab:: Typer-style

.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_typer.py


.. tab:: Typer-style w/finalize

.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_typer_ext.py


.. code-block:: console
$> ./manage.py finalizer cmd1 cmd1 cmd2
result1, result2, result3
.. tip::

@finalize() wrapped callbacks will be passed the CLI parameters on the current context
if the function signature accepts them. While convenient, we recommend using command state to
track these parameters instead. This will be more amenable to direct invocations of command
object functions.

Use @finalize on groups
~~~~~~~~~~~~~~~~~~~~~~~

Finalizers are hierarchical. The :func:`~django_typer.management.finalize` decorator is available
for use on subgroups. When used on a group, the callback will be invoked after the group's
subcommands have been executed and the return value of the finalizer will be passed up to any
finalizers at higher levels in the command hierarchy.

.. tabs::

.. tab:: Django-style

.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_group.py

.. tab:: Typer-style

.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_group_typer.py

.. tab:: Typer-style w/finalize

.. literalinclude:: ../../tests/apps/howto/management/commands/finalize_group_typer_ext.py

.. code-block:: console
$> ./manage.py finalizer cmd1 cmd1 cmd2 grp cmd4 cmd3
result1, result2, result3, RESULT4, RESULT3
.. tip::

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

This section is not yet implemented.

Call Commands from Code
-----------------------
Expand Down
4 changes: 2 additions & 2 deletions doc/source/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ django_typer
:show-inheritance:

.. autoclass:: django_typer.management.Typer
:members: callback, initialize, command, group, add_typer
:members: callback, initialize, finalize, command, group, add_typer

.. autoclass:: django_typer.management.TyperCommand
:members: initialize, callback, command, group, echo, secho, print_help, get_subcommand
:members: initialize, callback, finalize, command, group, echo, secho, print_help, get_subcommand

.. autoclass:: django_typer.management.CommandNode
:members: name, click_command, context, children, get_command, print_help
Expand Down
17 changes: 17 additions & 0 deletions tests/apps/howto/management/commands/finalize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import typing as t
from django_typer.management import TyperCommand, command, finalize


# chain=True allows multiple subroutines to be called from the command line
class Command(TyperCommand, chain=True):
@finalize()
def to_csv(self, results: t.List[str]):
return ", ".join(results)

@command()
def cmd1(self):
return "result1"

@command()
def cmd2(self):
return "result2"
37 changes: 37 additions & 0 deletions tests/apps/howto/management/commands/finalize_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import typing as t
from django_typer.management import TyperCommand, command, finalize, group


class Command(TyperCommand, chain=True):
"""
Show that finalizers are hierarchical and results are collected and
passed to the finalizer of the parent group if one exists.
"""

@finalize()
def to_csv(self, results: t.List[str]):
return ", ".join(results)

@command()
def cmd1(self):
return "result1"

@command()
def cmd2(self):
return "result2"

@group(chain=True)
def grp(self):
return "grp"

@grp.finalize()
def to_upper_csv(self, results):
return ", ".join([result.upper() for result in results])

@grp.command()
def cmd3(self):
return "result3"

@grp.command()
def cmd4(self):
return "result4"
36 changes: 36 additions & 0 deletions tests/apps/howto/management/commands/finalize_group_typer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from django_typer.management import Typer

# use native Typer interface to achieve the same result

def to_csv(results, **_):
return ", ".join(results)


def to_upper_csv(results, **_):
return ", ".join([result.upper() for result in results])


app = Typer(result_callback=to_csv, chain=True)

grp = Typer(result_callback=to_upper_csv, chain=True)
app.add_typer(grp, name="grp")


@app.command()
def cmd1():
return "result1"


@app.command()
def cmd2():
return "result2"


@grp.command()
def cmd3():
return "result3"


@grp.command()
def cmd4():
return "result4"
40 changes: 40 additions & 0 deletions tests/apps/howto/management/commands/finalize_group_typer_ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django_typer.management import Typer

# Use extensions to the typer interface to improve clarity

app = Typer(chain=True)


@app.finalize()
def to_csv(results):
return ", ".join(results)


@app.group(chain=True)
def grp():
pass


@grp.finalize()
def to_upper_csv(results):
return ", ".join([result.upper() for result in results])


@app.command()
def cmd1():
return "result1"


@app.command()
def cmd2():
return "result2"


@grp.command()
def cmd3():
return "result3"


@grp.command()
def cmd4():
return "result4"
20 changes: 20 additions & 0 deletions tests/apps/howto/management/commands/finalize_typer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django_typer.management import Typer


def to_csv(results, **_):
# result_callback is passed the CLI parameters on the current context
# if we are uninterested in them, we can use the **_ syntax to ignore them
return ", ".join(results)


app = Typer(result_callback=to_csv, chain=True)


@app.command()
def cmd1():
return "result1"


@app.command()
def cmd2():
return "result2"
22 changes: 22 additions & 0 deletions tests/apps/howto/management/commands/finalize_typer_ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django_typer.management import Typer

# alternatively we can use the finalize nomenclature of the TyperCommand
# interface - this is a non-standard Typer extension

app = Typer(chain=True)


# The Typer interface is extended with the finalize decorator
@app.finalize()
def to_csv(results):
return ", ".join(results)


@app.command()
def cmd1():
return "result1"


@app.command()
def cmd2():
return "result2"
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ def grp1_collect(self, result, **kwargs):
return f"grp1_collect: {self.grp1_final(result, **kwargs)}"


@FinalizeSubgroupsInherit.grp2.finalize()
def grp2_collect(result, **kwargs):
return f"grp2_collect: {FinalizeSubgroupsInherit.grp2_final(result, **kwargs)}"


@FinalizeSubgroupsInherit.grp1.command()
def cmd5(self):
return "cmd5"
10 changes: 5 additions & 5 deletions tests/test_finalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,15 +378,15 @@ def test_finalize_subgroups_inherit_run(self):
stdout, _, _ = run_command("finalize_subgroups_inherit", "grp2", "cmd4", "cmd3")
self.assertEqual(
stdout.strip(),
"root_final: grp2_final: ['cmd4', 'cmd3'] | g2_opt=True | init_opt=False",
"root_final: grp2_collect: grp2_final: ['cmd4', 'cmd3'] | g2_opt=True | init_opt=False",
)

stdout, _, _ = run_command(
"finalize_subgroups_inherit", "--init-opt", "grp2", "--no-g2-opt", "cmd3"
)
self.assertEqual(
stdout.strip(),
"root_final: grp2_final: ['cmd3'] | g2_opt=False | init_opt=True",
"root_final: grp2_collect: grp2_final: ['cmd3'] | g2_opt=False | init_opt=True",
)

stdout, _, _ = run_command(
Expand Down Expand Up @@ -420,7 +420,7 @@ def test_finalize_subgroups_inherit_call(self):
)
self.assertEqual(
out.getvalue().strip(),
"root_final: grp2_final: ['cmd4', 'cmd3'] | g2_opt=True | init_opt=False",
"root_final: grp2_collect: grp2_final: ['cmd4', 'cmd3'] | g2_opt=True | init_opt=False",
)

out = StringIO()
Expand All @@ -435,7 +435,7 @@ def test_finalize_subgroups_inherit_call(self):
)
self.assertEqual(
out.getvalue().strip(),
"root_final: grp2_final: ['cmd3'] | g2_opt=False | init_opt=True",
"root_final: grp2_collect: grp2_final: ['cmd3'] | g2_opt=False | init_opt=True",
)

out = StringIO()
Expand Down Expand Up @@ -521,5 +521,5 @@ def test_finalize_subgroups_inherit_obj(self):
],
init_opt=False,
).strip(),
"root_final: [\"grp2_final: ['cmd3', 'cmd4'] | g2_opt=False\"] | init_opt=False",
"root_final: [\"grp2_collect: grp2_final: ['cmd3', 'cmd4'] | g2_opt=False\"] | init_opt=False",
)
Loading

0 comments on commit a5a48ae

Please sign in to comment.