Skip to content

Commit

Permalink
fix #98
Browse files Browse the repository at this point in the history
  • Loading branch information
bckohan committed Jul 15, 2024
1 parent fbce52d commit cd78308
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 78 deletions.
25 changes: 13 additions & 12 deletions doc/source/architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ command callbacks as methods or static functions. Supporting dynamic command/gro
attributes on command instances also requires careful usage of advanced Python features.

The Typer_ app tree defines the layers of groups and commands that define the CLI. Each
:class:`~django_typer.TyperCommand` maintains its own app tree defined by a root
:class:`~django_typer.Typer` node. When other classes inherit from a base command class, that app
tree is copied and the new class can modify it without affecting the base class's tree. We extend
Typer_'s Typer type with our own :class:`~django_typer.Typer` class that adds additional
bookkeeping and attribute resolution features we need.
:class:`~django_typer.management.TyperCommand` maintains its own app tree defined by a root
:class:`~django_typer.management.Typer` node. When other classes inherit from a base command class,
that app tree is copied and the new class can modify it without affecting the base class's tree.
We extend Typer_'s Typer type with our own :class:`~django_typer.management.Typer` class that adds
additional bookkeeping and attribute resolution features we need.

django-typer_ must behave intuitively as expected and therefore it must support all of the
following:
Expand All @@ -37,8 +37,9 @@ method and if so, bind it to the correct class and pass the correct self instanc
test is :func:`~django_typer.utils.is_method` and simply checks to see if the function accepts
a first positional argument named `self`.

django-typer_ uses metaclasses to build the typer app tree when :class:`~django_typer.TyperCommand`
classes are instantiated. The logic flow proceeds this way:
django-typer_ uses metaclasses to build the typer app tree when
:class:`~django_typer.management.TyperCommand` classes are instantiated. The logic flow proceeds
this way:

- Class definition is read and @initialize/@callback, @group, @command decorators label and store
typer config and registration logic onto the function objects for processing later once the root
Expand All @@ -50,18 +51,18 @@ classes are instantiated. The logic flow proceeds this way:
included during this registration because they do not appear as attributes on the base classes.
This keeps inheritance pure while allowing plugins to not interfere. The exception to this is
when using the Typer-style interface where all commands and groups are registered dynamically.
A :class:`~django_typer.Typer` instance is passed as an argument to the
:class:`~django_typer.Typer` constructor and when this happens, the commands and groups will
be copied.
A :class:`~django_typer.management.Typer` instance is passed as an argument to the
:class:`~django_typer.management.Typer` constructor and when this happens, the commands and
groups will be copied.
- Metaclass __init__ sets the newly created Command class into the typer app tree and determines
if a common initializer needs to be added containing the default unsupressed django options.
- Command __init__ loads any registered plugins (this is a one time opperation that will happen
when the first Command of a given type is instantiated). It also determines if the addition
of any plugins should necessitate the addition of a common initializer and makes some last
attempts to pick the correct help from __doc__ if no help is present.

Below you can see that the backup inhertiance example :class:`~django_typer.Typer` tree. Each
command class has its own completely separate tree.
Below you can see that the backup inhertiance example :class:`~django_typer.management.Typer` tree.
Each command class has its own completely separate tree.

.. image:: /_static/img/inheritance_tree.png
:align: center
Expand Down
23 changes: 12 additions & 11 deletions doc/source/extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ Tutorial: Inheritance & Plugins
You may need to change the behavior of an
`upstream command <https://en.wikipedia.org/wiki/Upstream_(software_development)>`_ or wish
you could add an additional subcommand or group to it. django-typer_ offers two patterns for
changing or extending the behavior of commands. :class:`~django_typer.TyperCommand` classes
:ref:`support inheritance <inheritance>`, even multiple inheritance. This can be a way to override
or add additional commands to a command implemented elsewhere. You can then use Django's built in
command override precedence (INSTALLED_APPS) to ensure your command is used instead of the upstream
command or give it a different name if you would like the upstream command to still be available.
The :ref:`plugin pattern <plugin>` allows commands and groups to be added or overridden
changing or extending the behavior of commands. :class:`~django_typer.management.TyperCommand`
classes :ref:`support inheritance <inheritance>`, even multiple inheritance. This can be a way to
override or add additional commands to a command implemented elsewhere. You can then use Django's
built in command override precedence (INSTALLED_APPS) to ensure your command is used instead of the
upstream command or give it a different name if you would like the upstream command to still be
available. The :ref:`plugin pattern <plugin>` allows commands and groups to be added or overridden
directly on upstream commands without inheritance. This mechanism is useful when you might expect
other apps to also modify the original command. Conflicts are resolved in INSTALLED_APPS order.

Expand All @@ -27,8 +27,8 @@ but we anticipate our command being extended so we may also provide default beha
discover and run every backup routine defined on the command if no specific subroutine is invoked.
We can `use the context <https://typer.tiangolo.com/tutorial/commands/context/#getting-the-context>`_
to determine if a subcommand was called in our root initializer callback and we can find
subroutines added by plugins at runtime using :func:`~django_typer.TyperCommand.get_subcommand`.
Our command might look like this:
subroutines added by plugins at runtime using
:func:`~django_typer.management.TyperCommand.get_subcommand`. Our command might look like this:

.. tabs::

Expand Down Expand Up @@ -329,8 +329,8 @@ And the command line parameters to database have been removed:
The extension code is lazily loaded. This means plugins are resolved on command classes
the first time an instance of the class is instantiated. This avoids unnecessary code
execution but does mean that if you are working directly with the ``typer_app`` attribute
on a :class:`~django_typer.TyperCommand` you will need to make sure at least one instance
has been instantiated.
on a :class:`~django_typer.management.TyperCommand` you will need to make sure at least one
instance has been instantiated.


Overriding Groups
Expand Down Expand Up @@ -432,7 +432,8 @@ Plugins can be used to group like behavior together under a common root command.
thought of as a way to namespace CLI tools or easily share significant code between tools that have
common initialization logic. Moreover it allows you to do this safely and in a way that can be
deterministically controlled in settings. Most use cases are not this complex and even our backup
example could probably better be implemented as a batch of commands.
example could probably better be implemented as a
`batch of commands <https://github.com/bckohan/django-routines>`_.

Django apps are great for forcing separation of concerns on your code base. In large self contained
projects its often a good idea to break your code into apps that are as self contained as possible.
Expand Down
61 changes: 31 additions & 30 deletions doc/source/howto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Define Multiple Subcommands

Commands with a single executable function should simply implement handle(), but if you would
like have multiple subcommands you can define any number of functions decorated with
:func:`~django_typer.command` or :func:`~django_typer.Typer.command`:
:func:`~django_typer.management.command` or :func:`~django_typer.management.Typer.command`:

.. tabs::

Expand Down Expand Up @@ -196,8 +196,8 @@ Lets look at the help output:
Define Groups of Commands
-------------------------

Any depth of command tree can be defined. Use the :func:`~django_typer.group` or
:meth:`~django_typer.Typer.add_typer` decorator to define a group of subcommands:
Any depth of command tree can be defined. Use the :func:`~django_typer.management.group` or
:meth:`~django_typer.management.Typer.add_typer` decorator to define a group of subcommands:


.. tabs::
Expand Down Expand Up @@ -225,8 +225,8 @@ Define an Initialization Callback
---------------------------------

You can define an initializer function that takes arguments_ and options_ that will be invoked
before your handle() command or subcommands using the :func:`~django_typer.initialize` decorator.
This is like defining a group at the command root and is an extension of the
before your handle() command or subcommands using the :func:`~django_typer.management.initialize`
decorator. This is like defining a group at the command root and is an extension of the
`typer callback mechanism <https://typer.tiangolo.com/tutorial/commands/callback/>`_.


Expand Down Expand Up @@ -264,11 +264,11 @@ This is like defining a group at the command root and is an extension of the
Call Commands from Code
-----------------------

There are two options for invoking a :class:`~django_typer.TyperCommand` from code without spawning
off a subprocess. The first is to use Django_'s builtin call_command_ function. This function will
work exactly as it does for normal BaseCommand_ derived commands. django-typer_ however adds
another mechanism that can be more efficient, especially if your options and arguments are already
of the correct type and require no parsing:
There are two options for invoking a :class:`~django_typer.management.TyperCommand` from code
without spawning off a subprocess. The first is to use Django_'s builtin call_command_ function.
This function will work exactly as it does for normal BaseCommand_ derived commands. django-typer_
however adds another mechanism that can be more efficient, especially if your options and
arguments are already of the correct type and require no parsing:

Say we have this command, called ``mycommand``:

Expand Down Expand Up @@ -303,8 +303,8 @@ Say we have this command, called ``mycommand``:
The rule of thumb is this:

- Use call_command_ if your options and arguments need parsing.
- Use :func:`~django_typer.get_command` and invoke the command functions directly if your
options and arguments are already of the correct type.
- Use :func:`~django_typer.management.get_command` and invoke the command functions directly if
your options and arguments are already of the correct type.

If the second argument is a type, static type checking will assume the return value of get_command
to be of that type:
Expand All @@ -325,7 +325,7 @@ You may also fetch a subcommand function directly by passing its path:
.. tip::

Also refer to the :func:`~django_typer.get_command` docs and :ref:`here <default_cmd>`
Also refer to the :func:`~django_typer.management.get_command` docs and :ref:`here <default_cmd>`
and :ref:`here <multi_commands>` for the nuances of calling commands when handle() is and is
not implemented.

Expand All @@ -335,15 +335,15 @@ You may also fetch a subcommand function directly by passing its path:
Change Default Django Options
-----------------------------

:class:`~django_typer.TyperCommand` classes preserve all of the functionality of BaseCommand_ derivatives.
:class:`~django_typer.management.TyperCommand` classes preserve all of the functionality of BaseCommand_ derivatives.
This means that you can still use class members like `suppressed_base_arguments
<https://docs.djangoproject.com/en/5.0/howto/custom-management-commands/#django.core.management.BaseCommand.suppressed_base_arguments>`_
to suppress default options.

By default :class:`~django_typer.TyperCommand` suppresses ``--verbosity``. You can add it back by
setting ``suppressed_base_arguments`` to an empty list. If you want to use verbosity you can
simply redefine it or use one of django-typer_'s :ref:`provided type hints <types>` for the default
BaseCommand_ options:
By default :class:`~django_typer.management.TyperCommand` suppresses ``--verbosity``. You can add
it back by setting ``suppressed_base_arguments`` to an empty list. If you want to use verbosity you
can simply redefine it or use one of django-typer_'s :ref:`provided type hints <types>` for the
default BaseCommand_ options:

.. tabs::

Expand All @@ -366,9 +366,9 @@ Configure Typer_ Options
------------------------

Typer_ apps can be configured using a number of parameters. These parameters are usually passed
to the :class:`~django_typer.Typer` class constructor when the application is created.
to the :class:`~django_typer.management.Typer` class constructor when the application is created.
django-typer_ provides a way to pass these options upstream to Typer_ by supplying them as keyword
arguments to the :class:`~django_typer.TyperCommand` class inheritance:
arguments to the :class:`~django_typer.management.TyperCommand` class inheritance:

.. tabs::

Expand Down Expand Up @@ -398,9 +398,9 @@ arguments to the :class:`~django_typer.TyperCommand` class inheritance:
.. tip::

See :class:`~django_typer.TyperCommandMeta` or :class:`~django_typer.Typer` for a list of
available parameters. Also refer to the `Typer docs <https://typer.tiangolo.com>`_ for more
details.
See :class:`~django_typer.management.TyperCommandMeta` or
:class:`~django_typer.management.Typer` for a list of available parameters. Also refer to the
`Typer docs <https://typer.tiangolo.com>`_ for more details.


Define Shell Tab Completions for Parameters
Expand Down Expand Up @@ -658,8 +658,8 @@ Document Commands w/Sphinx
sphinxcontrib-typer_ can be used to render your rich helps to Sphinx docs and is used extensively
in this documentation.

For example, to document a :class:`~django_typer.TyperCommand` with sphinxcontrib-typer, you would
do something like this:
For example, to document a :class:`~django_typer.management.TyperCommand` with sphinxcontrib-typer,
you would do something like this:

.. code-block:: rst
Expand Down Expand Up @@ -698,17 +698,18 @@ There are no unbreakable rules about how you should print output from your comma
You could use loggers, normal print statements or the BaseCommand_ stdout and
stderr output wrappers. Django advises the use of ``self.stdout.write`` because the
stdout and stderr streams can be configured by calls to call_command_ or
:func:`~django_tyoer.get_command` which allows you to easily grab output from your
:func:`~django_typer.management.get_command` which allows you to easily grab output from your
commands for testing. Using the command's configured stdout and stderr
output wrappers also means output will respect the ``--force-color`` and ``--no-color``
parameters.

Typer_ and click_ provide `echo and secho <https://typer.tiangolo.com/tutorial/printing/>`_
functions that automatically handle byte to string conversions and offer simple styling
support. :class:`~django_typer.TyperCommand` provides :meth:`~django_typer.TyperCommand.echo` and
:meth:`~django_typer.TyperCommand.secho` wrapper functions for the Typer_ echo/secho
functions. If you wish to use Typer_'s echo you should use these wrapper functions because
they honor the command's ``--force-color`` and ``--no-color`` flags and the configured stdout/stderr
support. :class:`~django_typer.management.TyperCommand` provides
:meth:`~django_typer.management.TyperCommand.echo` and
:meth:`~django_typer.management.TyperCommand.secho` wrapper functions for the Typer_ echo/secho
functions. If you wish to use Typer_'s echo you should use these wrapper functions because they
honor the command's ``--force-color`` and ``--no-color`` flags and the configured stdout/stderr
streams:

.. tabs::
Expand Down
26 changes: 13 additions & 13 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ Django Typer
============

Use static typing to define the CLI for your Django_ management commands with Typer_. Optionally
use the provided :class:`~django_typer.TyperCommand` class that inherits from BaseCommand_. This
class maps the Typer_ interface onto a class based interface that Django developers will be
familiar with. All of the BaseCommand_ functionality is preserved, so that
:class:`~django_typer.TyperCommand` can be a drop in replacement.
use the provided :class:`~django_typer.management.TyperCommand` class that inherits from
BaseCommand_. This class maps the Typer_ interface onto a class based interface that Django
developers will be familiar with. All of the BaseCommand_ functionality is preserved, so that
:class:`~django_typer.management.TyperCommand` can be a drop in replacement.

**django-typer makes it easy to:**

Expand All @@ -22,7 +22,7 @@ familiar with. All of the BaseCommand_ functionality is preserved, so that
powershell_.
* :ref:`Create custom and portable shell tab-completions for your CLI parameters.
<define-shellcompletions>`
* Port existing commands (:class:`~django_typer.TyperCommand` is interface compatible
* Port existing commands (:class:`~django_typer.management.TyperCommand` is interface compatible
with BaseCommand_).
* Use either a Django-style class-based interface or the Typer-style interface to define
commands.
Expand Down Expand Up @@ -76,15 +76,15 @@ familiar with. All of the BaseCommand_ functionality is preserved, so that
This documentation shows all examples using both the function oriented Typer-style interface
and the class based Django-style interface in separate tabs. Each interface is functionally
equivalent so the choice of which to use is a matter of preference and familiarity. All
django-typer commands are instances of :class:`~django_typer.TyperCommand`, including commands
defined in the Typer-style interface. **This means you may always specify a self argument to
receive the instance of the command in your functions.**
django-typer commands are instances of :class:`~django_typer.management.TyperCommand`,
including commands defined in the Typer-style interface. **This means you may always specify a
self argument to receive the instance of the command in your functions.**

:big:`Basic Example`

:class:`~django_typer.TyperCommand` is a very simple drop in replacement for BaseCommand_. All of
the documented features of BaseCommand_ work the same way! Or, you may also use an interface
identical to Typer_'s. Simply import Typer_ from django_typer instead of typer.
:class:`~django_typer.management.TyperCommand` is a very simple drop in replacement for
BaseCommand_. All of the documented features of BaseCommand_ work the same way! Or, you may also
use an interface identical to Typer_'s. Simply import Typer_ from django_typer instead of typer.

.. tabs::

Expand Down Expand Up @@ -158,8 +158,8 @@ command like so:
Any number of groups and subcommands and subgroups of other groups can be defined allowing for
arbitrarily complex command hierarchies. The Typer-style interface builds a
:class:`~django_typer.TyperCommand` class for us **that allows you to optionally accept the self
argument in your commands.**
:class:`~django_typer.management.TyperCommand` class for us **that allows you to optionally accept
the self argument in your commands.**

.. tabs::

Expand Down
2 changes: 1 addition & 1 deletion doc/source/shell_completion.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Fish_ may not work at all in this mode.
Integrating with Other CLI Completion Libraries
-----------------------------------------------

When tab completion is requested for a command that is not a :class:`~django_typer.TyperCommand`,
When tab completion is requested for a command that is not a :class:`~django_typer.management.TyperCommand`,
django-typer_ will delegate that request to Django's
`autocomplete function <https://github.com/django/django/blob/main/django/core/management/__init__.py#L278>`_
as a fallback. This means that using django-typer_ to install completion scripts will enable
Expand Down
Loading

0 comments on commit cd78308

Please sign in to comment.