|
18 | 18 |
|
19 | 19 | django-typer_ also supports shell completion for bash_, zsh_, fish_ and powershell_ and
|
20 | 20 | extends that support to native Django_ management commands as well.
|
| 21 | +
|
| 22 | +
|
| 23 | +The goal of django-typer_ is to provide full typer style functionality while maintaining |
| 24 | +compatibility with the Django management command system. This means that the BaseCommand |
| 25 | +interface is preserved and the Typer_ interface is added on top of it. This means that |
| 26 | +this code base is more robust to changes in the Django management command system - because |
| 27 | +most of the base class functionality is preserved but many Typer_ and click_ internals are |
| 28 | +used directly to achieve this. We rely on robust CI to catch breaking changes upstream. |
21 | 29 | """
|
22 | 30 |
|
23 | 31 | # During development of django-typer_ I've wrestled with a number of encumbrances in the
|
@@ -586,13 +594,15 @@ def command( # type: ignore
|
586 | 594 |
|
587 | 595 | .. code-block:: python
|
588 | 596 |
|
589 |
| - @group() |
590 |
| - def group1(): |
591 |
| - pass |
| 597 | + class Command(TyperCommand): |
592 | 598 |
|
593 |
| - @group1.command() |
594 |
| - def command1(): |
595 |
| - # do stuff here |
| 599 | + @group() |
| 600 | + def group1(self): |
| 601 | + pass |
| 602 | +
|
| 603 | + @group1.command() |
| 604 | + def command1(self): |
| 605 | + # do stuff here |
596 | 606 |
|
597 | 607 | .. note::
|
598 | 608 |
|
@@ -664,17 +674,19 @@ def group(
|
664 | 674 |
|
665 | 675 | .. code-block:: python
|
666 | 676 |
|
667 |
| - @group() |
668 |
| - def group1(): |
669 |
| - pass |
| 677 | + class Command(TyperCommand): |
670 | 678 |
|
671 |
| - @group1.group() |
672 |
| - def subgroup(): |
673 |
| - # do common group init stuff here |
| 679 | + @group() |
| 680 | + def group1(self): |
| 681 | + pass |
| 682 | +
|
| 683 | + @group1.group() |
| 684 | + def subgroup(self): |
| 685 | + # do common group init stuff here |
674 | 686 |
|
675 |
| - @subgroup.command(help=_('My command does good stuff!')) |
676 |
| - def subcommand(): |
677 |
| - # do command stuff here |
| 687 | + @subgroup.command(help=_('My command does good stuff!')) |
| 688 | + def subcommand(self): |
| 689 | + # do command stuff here |
678 | 690 |
|
679 | 691 |
|
680 | 692 | :param name: the name of the group
|
@@ -901,23 +913,27 @@ def command( # pylint: disable=keyword-arg-before-vararg
|
901 | 913 |
|
902 | 914 | .. code-block:: python
|
903 | 915 |
|
904 |
| - @command(epilog="This is the epilog for the command.") |
905 |
| - def handle(): |
906 |
| - ... |
| 916 | + class Command(TyperCommand): |
| 917 | +
|
| 918 | + @command(epilog="This is the epilog for the command.") |
| 919 | + def handle(self): |
| 920 | + ... |
907 | 921 |
|
908 | 922 | We can also use the command decorator to define multiple subcommands:
|
909 | 923 |
|
910 | 924 | .. code-block:: python
|
911 | 925 |
|
912 |
| - @command() |
913 |
| - def command1(): |
914 |
| - # execute command1 logic here |
| 926 | + class Command(TyperCommand): |
| 927 | +
|
| 928 | + @command() |
| 929 | + def command1(self): |
| 930 | + # execute command1 logic here |
915 | 931 |
|
916 |
| - @command(name='command2') |
917 |
| - def other_command(): |
918 |
| - # arguments passed to the decorator are passed to typer and control |
919 |
| - # various aspects of the command, for instance here we've changed the |
920 |
| - # name of the command to 'command2' from 'other_command' |
| 932 | + @command(name='command2') |
| 933 | + def other_command(self): |
| 934 | + # arguments passed to the decorator are passed to typer and control |
| 935 | + # various aspects of the command, for instance here we've changed the |
| 936 | + # name of the command to 'command2' from 'other_command' |
921 | 937 |
|
922 | 938 | The decorated function is the command function. It may also be invoked directly
|
923 | 939 | as a method from an instance of the :class:`~django_typer.TyperCommand` class,
|
@@ -1001,24 +1017,35 @@ def group(
|
1001 | 1017 | .. code-block:: python
|
1002 | 1018 | :caption: management/commands/example.py
|
1003 | 1019 |
|
1004 |
| - @group() |
1005 |
| - def group1(flag: bool = False): |
1006 |
| - # do group init stuff here |
| 1020 | + from django_typer import TyperCommand, group |
| 1021 | +
|
| 1022 | + class Command(TyperCommand): |
| 1023 | +
|
| 1024 | + @group() |
| 1025 | + def group1(self, flag: bool = False): |
| 1026 | + # do group init stuff here |
| 1027 | +
|
| 1028 | + # to attach a command to the group, use the command() decorator |
| 1029 | + # on the group function |
| 1030 | + @group1.command() |
| 1031 | + def command1(self): |
| 1032 | + ... |
| 1033 | +
|
| 1034 | + # you can also attach subgroups to groups! |
| 1035 | + @group1.group() |
| 1036 | + def subgroup(self): |
| 1037 | + # do subgroup init stuff here |
| 1038 | +
|
| 1039 | + @subgroup.command() |
| 1040 | + def subcommand(self): |
| 1041 | + ... |
1007 | 1042 |
|
1008 |
| - # to attach a command to the group, use the command() decorator |
1009 |
| - # on the group function |
1010 |
| - @group1.command() |
1011 |
| - def command1(): |
1012 |
| - # this would be invoked like: ./manage.py example group1 --flag command1 |
| 1043 | + These groups and subcommands can be invoked from the command line like so: |
1013 | 1044 |
|
1014 |
| - # you can also attach subgroups to groups! |
1015 |
| - @group1.group() |
1016 |
| - def subgroup(): |
1017 |
| - # do subgroup init stuff here |
| 1045 | + .. code-block:: bash |
1018 | 1046 |
|
1019 |
| - @subgroup.command() |
1020 |
| - def subcommand(): |
1021 |
| - # this would be invoked like: ./manage.py example group1 --flag subgroup subcommand |
| 1047 | + $ ./manage.py example group1 --flag command1 |
| 1048 | + $ ./manage.py example group1 --flag subgroup subcommand |
1022 | 1049 |
|
1023 | 1050 | :param name: the name of the group (defaults to the name of the decorated function)
|
1024 | 1051 | :param cls: the group class to use
|
@@ -1070,7 +1097,7 @@ def create_app(func: CommandFunctionType):
|
1070 | 1097 | return create_app
|
1071 | 1098 |
|
1072 | 1099 |
|
1073 |
| -class _TyperCommandMeta(type): |
| 1100 | +class TyperCommandMeta(type): |
1074 | 1101 | """
|
1075 | 1102 | The metaclass used to build the TyperCommand class. This metaclass is responsible
|
1076 | 1103 | for building Typer app using the arguments supplied to the TyperCommand constructor.
|
@@ -1514,41 +1541,128 @@ class write method.
|
1514 | 1541 | return super().write(msg=msg, style_func=style_func, ending=ending)
|
1515 | 1542 |
|
1516 | 1543 |
|
1517 |
| -class TyperCommand(BaseCommand, metaclass=_TyperCommandMeta): |
| 1544 | +class TyperCommand(BaseCommand, metaclass=TyperCommandMeta): |
1518 | 1545 | """
|
1519 |
| - A BaseCommand extension class that uses the Typer library to parse |
1520 |
| - arguments and options. This class adapts BaseCommand using a light touch |
1521 |
| - that relies on most of the original BaseCommand implementation to handle |
| 1546 | + An extension of BaseCommand_ that uses the Typer_ library to parse |
| 1547 | + arguments and options. This class adapts BaseCommand_ using a light touch |
| 1548 | + that relies on most of the original BaseCommand_ implementation to handle |
1522 | 1549 | default arguments and behaviors.
|
1523 | 1550 |
|
1524 |
| - The goal of django-typer is to provide full typer style functionality |
1525 |
| - while maintaining compatibility with the Django management command system. |
1526 |
| - This means that the BaseCommand interface is preserved and the Typer |
1527 |
| - interface is added on top of it. This means that this code base is more |
1528 |
| - robust to changes in the Django management command system - because most |
1529 |
| - of the base class functionality is preserved but many typer and click |
1530 |
| - internals are used directly to achieve this. We rely on robust CI to |
1531 |
| - catch breaking changes upstream. |
1532 |
| -
|
1533 |
| - TyperCommands are built in stages. The metaclass is responsible for finding |
1534 |
| - all the commands and callbacks and building the Typer app. This happens at |
1535 |
| - class definition time (i.e. on module load). When the TyperCommand is instantiated |
1536 |
| - the command tree is built thats used for subcommand resolution in django-typer's |
1537 |
| - get_command method and for help output. |
1538 |
| -
|
1539 |
| - All of the documented BaseCommand_ functionality works as expected. call_command() |
| 1551 | + All of the documented BaseCommand_ functionality works as expected. call_command_ |
1540 | 1552 | also works as expected. TyperCommands however add a few extra features:
|
1541 | 1553 |
|
1542 |
| - - Simple TyperCommands implemented only using handle() can be invoked directly |
1543 |
| - as a function. |
1544 |
| - - Subcommands can be fetched and invoked directly as functions using get_command() |
| 1554 | + - We define arguments_ and options_ using concise and optionally annotated type hints. |
| 1555 | + - Simple TyperCommands implemented only using handle() can be called directly |
| 1556 | + by invoking the command as a callable. |
| 1557 | + - We can define arbitrarily complex subcommand group hierarchies using the |
| 1558 | + :func:`~django_typer.group` and :func:`~django_typer.command` decorators. |
| 1559 | + - Commands and subcommands can be fetched and invoked directly as functions using |
| 1560 | + :func:`~django_typer.get_command` |
| 1561 | + - We can define common initialization logic for groups of commands using |
| 1562 | + :func:`~django_typer.initialize` |
| 1563 | + - TyperCommands may safely return non-string values from handle() |
| 1564 | +
|
| 1565 | + Defining a typer command is a lot like defining a BaseCommand_ except that we do not |
| 1566 | + have an add_arguments() method. Instead we define the parameters using type hints |
| 1567 | + directly on handle(): |
| 1568 | +
|
| 1569 | + .. code-block:: python |
| 1570 | +
|
| 1571 | + import typing as t |
| 1572 | + from django_typer import TyperCommand |
| 1573 | +
|
| 1574 | + class Command(TyperCommand): |
| 1575 | +
|
| 1576 | + def handle( |
| 1577 | + self, |
| 1578 | + arg: str, |
| 1579 | + option: t.Optional[str] = None |
| 1580 | + ): |
| 1581 | + # do command logic here |
| 1582 | +
|
| 1583 | + TyperCommands can be extremely simple like above, or we can create really complex |
| 1584 | + command group hierarchies with subcommands and subgroups (see :func:`~django_typer.group` |
| 1585 | + and :func:`~django_typer.command`). |
| 1586 | +
|
| 1587 | + Typer_ apps can be configured with a number of parameters to control behavior such as |
| 1588 | + exception behavior, help output, help markup interpretation, result processing and |
| 1589 | + execution flow. These parameters can be passed to typer as keyword arguments in your |
| 1590 | + Command class inheritance: |
| 1591 | +
|
| 1592 | + .. code-block:: python |
| 1593 | + :caption: management/commands/chain.py |
| 1594 | + :linenos: |
| 1595 | +
|
| 1596 | + import typing as t |
| 1597 | + from django_typer import TyperCommand, command |
| 1598 | +
|
| 1599 | +
|
| 1600 | + class Command(TyperCommand, rich_markup_mode='markdown', chain=True): |
| 1601 | +
|
| 1602 | + suppressed_base_arguments = [ |
| 1603 | + '--verbosity', '--traceback', '--no-color', '--force-color', |
| 1604 | + '--skip_checks', '--settings', '--pythonpath', '--version' |
| 1605 | + ] |
| 1606 | +
|
| 1607 | + @command() |
| 1608 | + def command1(self, option: t.Optional[str] = None): |
| 1609 | + \"""This is a *markdown* help string\""" |
| 1610 | + print('command1') |
| 1611 | + return option |
| 1612 | +
|
| 1613 | + @command() |
| 1614 | + def command2(self, option: t.Optional[str] = None): |
| 1615 | + \"""This is a *markdown* help string\""" |
| 1616 | + print('command2') |
| 1617 | + return option |
| 1618 | +
|
| 1619 | +
|
| 1620 | + We're doing a number of things here: |
| 1621 | +
|
| 1622 | + - Using the :func:`~django_typer.command` decorator to define multiple subcommands. |
| 1623 | + - Using the `suppressed_base_arguments attribute |
| 1624 | + <https://docs.djangoproject.com/en/stable/howto/custom-management-commands/#django.core.management.BaseCommand.suppressed_base_arguments>`_ |
| 1625 | + to suppress the default options Django adds to the command interface. |
| 1626 | + - Using the `rich_markup_mode parameter |
| 1627 | + <https://typer.tiangolo.com/tutorial/commands/help/#rich-markdown-and-markup>`_ to enable |
| 1628 | + markdown rendering in help output. |
| 1629 | + - Using the chain parameter to enable command chaining. |
| 1630 | +
|
| 1631 | +
|
| 1632 | + We can see that our help renders like so: |
| 1633 | +
|
| 1634 | + .. typer:: django_typer.tests.test_app.management.commands.chain.Command:typer_app |
| 1635 | + :prog: ./manage.py chain |
| 1636 | + :width: 80 |
| 1637 | + :convert-png: latex |
| 1638 | +
|
| 1639 | +
|
| 1640 | + And we can see the chain behavior by calling our command(s) like so: |
| 1641 | +
|
| 1642 | + .. code-block:: bash |
| 1643 | +
|
| 1644 | + $ ./manage.py chain command1 --option one command2 --option two |
| 1645 | + command1 |
| 1646 | + command2 |
| 1647 | + ['one', 'two'] |
| 1648 | +
|
| 1649 | + See :class:`~django_typer.TyperCommandMeta` for the list of accepted parameters. Also |
| 1650 | + refer to the Typer_ docs for more information on the behaviors expected for |
| 1651 | + those parameters - they are passed through to the Typer class constructor. Not all |
| 1652 | + parameters may make sense in the context of a django command. |
1545 | 1653 |
|
1546 | 1654 | :param stdout: the stdout stream to use
|
1547 | 1655 | :param stderr: the stderr stream to use
|
1548 | 1656 | :param no_color: whether to disable color output
|
1549 | 1657 | :param force_color: whether to force color output even if the stream is not a tty
|
1550 | 1658 | """
|
1551 | 1659 |
|
| 1660 | + # TyperCommands are built in stages. The metaclass is responsible for finding |
| 1661 | + # all the commands and callbacks and building the Typer_ app. This happens at |
| 1662 | + # class definition time (i.e. on module load). When the TyperCommand is instantiated |
| 1663 | + # the command tree is built thats used for subcommand resolution in django-typer's |
| 1664 | + # get_command method and for help output. |
| 1665 | + |
1552 | 1666 | style: ColorStyle
|
1553 | 1667 | stdout: BaseOutputWrapper
|
1554 | 1668 | stderr: BaseOutputWrapper
|
|
0 commit comments