Skip to content

Commit

Permalink
Add more information to CLI app guide.
Browse files Browse the repository at this point in the history
  • Loading branch information
aholmes committed Jan 23, 2025
1 parent 8be9283 commit ddec450
Showing 1 changed file with 125 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,56 +24,125 @@ First, let's add some imports that our application depends on.
from injector import inject
from Ligare.programming.application import ApplicationBase, ApplicationBuilder
from Ligare.programming.config import Config
from typing_extensions import override
We're going to write an application that displays a message stored in a configuration file.

To do this, we need to do a few things:

1. Create an ``Application`` class
2. Create a ``Config`` class
2. Create a ``Config`` class and file
3. Configure the application

Creating the Application Class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

All Ligare applications are extensions of ``ApplicationBase``. These classes must implement a ``run`` method, which can use the Ligare dependency injection system.

For our console application, we're going to print a message from an instance of a ``Config`` class.
All Ligare applications are extensions of ``ApplicationBase``. These classes must implement a
``run`` method, which can use the Ligare dependency injection system.

.. code-block:: python
class Application(ApplicationBase):
@inject
@override
def run(self, config: "AppConfig"):
print(config.message)
def run(self):
print("Hello world!")
input("\nPress anything to exit. ")
Here, we have created a class called ``Application`` that extends ``ApplicationBase``.
We then implement the run method with a single parameter named ``config``. The use of
``@inject`` ensures that the run method receives an instance of ``AppConfig`` as the
value of ``config``.

There is no need to instantiate ``Application``, no need to call ``run``, and no need
to pass in the value of ``config`` ourselves - Ligare will do this for us.

To set up Ligare to run our application, we will use ``ApplicationBuilder``.
Although we could run our application by instantiating ``Application`` and calling ``run``,
we would lose out on what Ligare care do for us. To take advantage of that functionality,
we will use ``ApplicationBuilder``.

.. code-block:: python
builder = ApplicationBuilder(Application)
This gives us a builder for our ``Application`` class with which we can configure the
application, and then start it. While there are a variety of options we can set,
this guide will only go over ``use_configuration``.
application, and then start it.

This is the minimum required to set up our application. We can now "build" it,
and then start it.


.. code-block:: python
application = builder.build()
if __name__ == "__main__":
application.run()
Our completed application looks like this.

.. code-block:: python
from injector import inject
from Ligare.programming.application import ApplicationBase, ApplicationBuilder
from typing_extensions import override
class Application(ApplicationBase):
@inject
@override
def run(self):
print("Hello, world!")
input("\nPress anything to exit. ")
builder = ApplicationBuilder(Application)
application = builder.build()
if __name__ == "__main__":
application.run()
When we run the application, we see the expected output.

.. code-block:: shell-session
user@: my-ligare-app $ python -m app
Hello, world!
Press anything to exit.
Creating the Config Class
^^^^^^^^^^^^^^^^^^^^^^^^^

Now let's see how to create the ``Config`` class and make better use of Ligare.

Here we will see:

* How to use Ligare's dependency injection system
* How to set up application configuration with a file
* How to use configuration values from a configuration file

Start by importing Ligare's base ``Config`` class.

.. code-block:: python
from Ligare.programming.config import Config
To create a ``Config`` class for our application, we need to extend ``Config``
and add any fields to it that we expect to be in a configuration file, and
that we want the application to be able to read.

.. code-block:: python
class AppConfig(Config):
message: str
Because we want to display a message from a configuration file, we just add
a "message" field to our class. We will not set this value directly; this class
is a Pydantic dataclass whose values Ligare will set from a TOML file.

Registering the Config Class
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For Ligare to know about this class, and to tell Ligare where our configuration
file is, we use the ``use_configuration`` method from the ``ApplicationBuilder``
instance to "register" the class with Ligare's dependency injection system.
This must occur before ``builder.build()`` is called.

.. code-block:: python
builder.use_configuration(
lambda config_builder: config_builder \
.with_config_type(AppConfig) \
Expand All @@ -85,56 +154,60 @@ That parameter type is itself an ``ApplicationConfigBuilder`` that is used to se
options specific to the configuration of a Ligare application.
`It's builders all the way down! <https://en.wikipedia.org/wiki/Turtles_all_the_way_down>`_

First we create our ``Config`` class, ``AppConfig``. This class is a Pydantic dataclass
whose values Ligare will set from a TOML file.
Using the Config Class
~~~~~~~~~~~~~~~~~~~~~~

Then, within the function passed to ``use_configuration``, we set both the type of our
config class, and the path to the TOML file.
Now we need to adjust our run method.

This is the minimum required to set up our application. We can now "build" it,
and then start it.
.. code-block:: python
class Application(ApplicationBase):
@inject # 1
@override
def run(self, config: "AppConfig"): # 2
print(config.message) # 3
input("\nPress anything to exit. ")
.. code-block:: python
There are three notable changes here:

application = builder.build()
1. The ``@inject`` decorator was added to the run method
2. The ``config`` parameter was added to the run method
3. The ``config`` object is used to print a field value called ``message``

if __name__ == "__main__":
application.run()
Ligare supports the use of `Dependency Injection <https://en.wikipedia.org/wiki/Dependency_injection>`_
so you can use certain objects throughout your application without having to instantiate them yourself.
In this case, ``run(config)`` is called automatically by Ligare. Because the ``@inject`` decorator is present (#1),
the value of ``config`` is created automatically, and it is passed into your run method.

We then need to specify what the type of the parameter is (#2); in this case, the type is ``AppConfig``.
This is accomplished with Python's `type annotation <https://docs.python.org/3/library/typing.html>`_ system,
by writing ``config: "AppConfig"``.

Finally, we use the value of ``config`` to access the field ``message``, and pass that into ``print``. (#3)

.. First, ``Ligare.web`` uses a `builder <https://en.wikipedia.org/wiki/Builder_pattern>`_ to set up an instance of your application during runtime.
.. This allows us to `build` our application using specific functions to configure desired beavhiors and features.
..
.. .. seealso::
..
.. A `builder <https://en.wikipedia.org/wiki/Builder_pattern>`_ is a pattern that helps programmers configure an instance of some other `type`.
.. For this guide, it helps us configure an instance of ``FlaskApp``, which is the
.. class name for the type of application we're creating.
..
..
.. The class we need for this is :obj:`ApplicationBuilder[T] <Ligare.web.application.ApplicationBuilder>`. We also need the application type. For this example, we're using `FlaskApp <https://github.com/spec-first/connexion/blob/3450a602fdd43b1d3a06581fd0106397b32e965b/connexion/apps/flask.py#L166>`_.
..
.. .. seealso::
..
.. A "generic" class is a way to specify `generic` functionality for a range of more specific types.
.. Here, we use the generic class :obj:`ApplicationBuilder[T] <Ligare.web.application.ApplicationBuilder>` to configure `generic` aspects of
.. ``FlaskApp``.
.. Here, the "generic type" is named ``T`` and is how we reference the more specific type that
.. it represents.
..
.. .. note::
..
.. :obj:`ApplicationBuilder[T] <Ligare.web.application.ApplicationBuilder>` supports one other type of class called ``Flask``.
.. Review `this guide <it doesn't exist yet>`_ to see how to create a ``Flask`` application.
With these changes, your application should resemble the Ligare example CLI application.

.. literalinclude:: ../../../../../examples/cli-app/app/__main__.py
:language: python
:caption: :example:`cli-app/app/__main__.py`

Now we have something we can run. Go ahead and run this code.
Creating the Config File
^^^^^^^^^^^^^^^^^^^^^^^^

There is one last thing to do, which is to create the configuration file and store our message in it.

.. code-block:: shell-session
user@: my-ligare-app $ cat > app/config.toml <<EOF
[app]
message = "Hello, world!"
EOF
Now we can run our application to see the message stored in the configuration file.

.. code-block:: shell-session
user@: my-ligare-app $ python -m app
Hello, world!
Press anything to exit.

0 comments on commit ddec450

Please sign in to comment.