@@ -688,6 +688,10 @@ class Finalizer(t.Generic[P, R]):
688
688
finalizer : t .Callable [P , R ]
689
689
is_method : bool
690
690
691
+ @property
692
+ def name (self ):
693
+ return self .finalizer .__name__
694
+
691
695
def __init__ (self , finalizer : t .Callable [P , R ]):
692
696
self .finalizer = finalizer
693
697
self .is_method = bool (is_method (finalizer ))
@@ -1361,6 +1365,21 @@ def create_app(func: t.Callable[P2, R2]) -> Typer[P2, R2]:
1361
1365
1362
1366
return create_app
1363
1367
1368
+ def finalize (self ):
1369
+ """
1370
+ Add a finalizer callback to collect the results of all commands run as part of this
1371
+ command group. This is analogous to the ``result_callback`` on ``Typer.add_typer`` but
1372
+ works seamlessly for methods. See :ref:`howto_finalizers` for more information.
1373
+ """
1374
+
1375
+ def create_finalizer (func : t .Callable [P2 , R2 ]) -> t .Callable [P2 , R2 ]:
1376
+ func = _strip_static (func )
1377
+ # TODO not this easy: setattr(self, func.__name__, func)
1378
+ self .info .result_callback = Finalizer (func )
1379
+ return func
1380
+
1381
+ return create_finalizer
1382
+
1364
1383
1365
1384
class BoundProxy (t .Generic [P , R ]):
1366
1385
"""
@@ -1369,7 +1388,7 @@ class BoundProxy(t.Generic[P, R]):
1369
1388
"""
1370
1389
1371
1390
command : "TyperCommand"
1372
- proxied : TyperFunction
1391
+ proxied : t . Union [ TyperFunction , Finalizer ]
1373
1392
1374
1393
def __init__ (self , command : "TyperCommand" , proxied : TyperFunction ):
1375
1394
self .command = command
@@ -1379,6 +1398,8 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
1379
1398
if isinstance (self .proxied , Typer ) and not self .proxied .parent :
1380
1399
# if we're calling a top level Typer app we need invoke Typer's call
1381
1400
return self .proxied (* args , ** kwargs )
1401
+ elif isinstance (self .proxied , Finalizer ):
1402
+ return self .proxied (* args , _command = self .command , ** kwargs )
1382
1403
return _get_direct_function (self .command , self .proxied )(* args , ** kwargs )
1383
1404
1384
1405
def __getattr__ (self , name : str ) -> t .Any :
@@ -1393,6 +1414,10 @@ def __getattr__(self, name: str) -> t.Any:
1393
1414
if isinstance (attr , (Typer , typer .models .CommandInfo )):
1394
1415
return BoundProxy (self .command , attr )
1395
1416
return attr
1417
+ elif isinstance (self .proxied , Typer ) and isinstance (
1418
+ self .proxied .info .result_callback , Finalizer
1419
+ ):
1420
+ return BoundProxy (self .command , self .proxied .info .result_callback )
1396
1421
1397
1422
raise AttributeError (
1398
1423
"{cls} object has no attribute {name}" .format (
@@ -1548,7 +1573,36 @@ def make_initializer(func: t.Callable[P2, R2]) -> t.Callable[P2, R2]:
1548
1573
1549
1574
def finalize () -> t .Callable [[t .Callable [P2 , R2 ]], t .Callable [P2 , R2 ]]:
1550
1575
"""
1551
- TODO
1576
+ Attach a callback function that collects and finalizes results returned
1577
+ from any invoked subcommands. This is a wrapper around typer's result_callback that
1578
+ extends it to work as a method and for non-compound commands.
1579
+
1580
+ .. code-block:: python
1581
+
1582
+ class Command(TyperCommand, chain=True):
1583
+
1584
+ @finalize()
1585
+ def collect(self, results: t.List[str]):
1586
+ return ", ".join(results)
1587
+
1588
+ @command()
1589
+ def cmd1(self):
1590
+ return "result1"
1591
+
1592
+ @command()
1593
+ def cmd2(self):
1594
+ return "result2"
1595
+
1596
+ .. code-block:: bash
1597
+
1598
+ $ ./manage.py command cmd1 cmd2
1599
+ result1, result2
1600
+
1601
+ The callback must at minimum accept a single argument that is either a list of
1602
+ results or a singular result if the command is not compound. The callback may
1603
+ optionally accept named arguments that correspond to parameters at the given
1604
+ command group scope. These parameters will also be available on the current
1605
+ context.
1552
1606
"""
1553
1607
1554
1608
def make_finalizer (func : t .Callable [P2 , R2 ]) -> t .Callable [P2 , R2 ]:
@@ -1839,7 +1893,7 @@ def _names(tc: t.Union[typer.models.CommandInfo, Typer]) -> t.List[str]:
1839
1893
"""
1840
1894
For a command or group, get a list of attribute name and its CLI name.
1841
1895
1842
- This annoyingly lives in difference places depending on how the command
1896
+ This annoyingly lives in different places depending on how the command
1843
1897
or group was defined. This logic is sensitive to typer internals.
1844
1898
"""
1845
1899
names = []
@@ -1858,7 +1912,7 @@ def _names(tc: t.Union[typer.models.CommandInfo, Typer]) -> t.List[str]:
1858
1912
1859
1913
def _bfs_match (
1860
1914
app : Typer , name : str
1861
- ) -> t .Optional [t .Union [typer .models .CommandInfo , Typer ]]:
1915
+ ) -> t .Optional [t .Union [typer .models .CommandInfo , Typer , Finalizer ]]:
1862
1916
"""
1863
1917
Perform a breadth first search for a command or group by name.
1864
1918
@@ -1869,12 +1923,18 @@ def _bfs_match(
1869
1923
1870
1924
def find_at_level (
1871
1925
lvl : Typer ,
1872
- ) -> t .Optional [t .Union [typer .models .CommandInfo , Typer ]]:
1926
+ ) -> t .Optional [t .Union [typer .models .CommandInfo , Typer , Finalizer ]]:
1873
1927
for cmd in reversed (lvl .registered_commands ):
1874
1928
if name in _names (cmd ):
1875
1929
return cmd
1876
1930
if name in _names (lvl ):
1877
1931
return lvl
1932
+
1933
+ if (
1934
+ isinstance (lvl .info .result_callback , Finalizer )
1935
+ and lvl .info .result_callback .name == name
1936
+ ):
1937
+ return lvl .info .result_callback
1878
1938
return None
1879
1939
1880
1940
# fast exit out if at top level (most searches - avoid building BFS)
@@ -2191,7 +2251,7 @@ def __init__(self, cls_name, bases, attrs, **kwargs):
2191
2251
def __getattr__ (cls , name : str ) -> t .Any :
2192
2252
"""
2193
2253
Fall back breadth first search of the typer app tree to resolve attribute accesses of the type:
2194
- Command.sub_grp or Command.sub_cmd
2254
+ Command.sub_grp or Command.sub_cmd or Command.finalizer
2195
2255
"""
2196
2256
if name != "typer_app" :
2197
2257
if called_from_command_definition ():
@@ -2740,6 +2800,33 @@ def make_initializer(func: t.Callable[P, R]) -> t.Callable[P, R]:
2740
2800
2741
2801
callback = initialize
2742
2802
2803
+ @classmethod
2804
+ def finalize (cls ) -> t .Callable [[t .Callable [P2 , R2 ]], t .Callable [P2 , R2 ]]:
2805
+ """
2806
+ Override the finalizer for this command class after it has been defined.
2807
+
2808
+ .. note::
2809
+ See :ref:`plugins` for details on when you might want to use this extension
2810
+ pattern.
2811
+
2812
+ .. code-block:: python
2813
+
2814
+ from your_app.management.commands.your_command import Command as YourCommand
2815
+
2816
+ @YourCommand.finalize()
2817
+ def collect(self, ...):
2818
+ # implement your command result collection logic here
2819
+ """
2820
+ if called_from_command_definition ():
2821
+ return finalize ()
2822
+
2823
+ def make_finalizer (func : t .Callable [P2 , R2 ]) -> t .Callable [P2 , R2 ]:
2824
+ setattr (cls , func .__name__ , func )
2825
+ cls .typer_app .info .result_callback = Finalizer (_strip_static (func ))
2826
+ return func
2827
+
2828
+ return make_finalizer
2829
+
2743
2830
@classmethod
2744
2831
def command (
2745
2832
cmd , # pyright: ignore[reportSelfClsParameterName]
0 commit comments