Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions craft_application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,52 @@ class AppFeatures:
@final
@dataclass(frozen=True)
class AppMetadata:
"""Metadata about a *craft application."""
"""Metadata about an application that uses this framework."""

name: str
"""The name of the application.

This value is expected to be the executable name and its module name..
"""
summary: str | None = None
"""A brief summary of the application.

Normally a multi-line string. This string is used in the ``help`` command.
"""
version: str = field(init=False)
"""The version of the application.

Automatically determined from the ``__version__`` string of the app's module.
"""
docs_url: str | None = None
"""A string template for the documentation URL.

Can include ``{version}`` to render to a specific version.
"""
source_ignore_patterns: list[str] = field(default_factory=list)
"""A list of glob patterns to ignore in the source directory.

This usually includes the <app>.yaml file and any output artifacts.
"""
managed_instance_project_path = pathlib.PurePosixPath("/root/project")
"""The project path when running inside a managed instance."""
features: AppFeatures = AppFeatures()
project_variables: list[str] = field(default_factory=lambda: ["version"])
"""A list of field names from the project model that can be added using adopt-info.

Each field here needs to be optional on the project class and have a default value.
"""
mandatory_adoptable_fields: list[str] = field(default_factory=lambda: ["version"])
ConfigModel: type[_config.ConfigModel] = _config.ConfigModel
"""A list of field names that, if adopted, are still mandatory.

These fields are expected to be filled using `craftctl set` in an override script
of the relevant adopting part. They will be checked for a value before packing the
final artifact.
"""
ConfigModel: type[_config.ConfigModel] = _config.ConfigModel
"""The model to use for configuring this application."""
ProjectClass: type[models.Project] = models.Project
"""The Project model class to use for this application."""
BuildPlannerClass: type[models.BuildPlanner] = models.BuildPlanner

def __post_init__(self) -> None:
Expand Down Expand Up @@ -116,7 +148,9 @@ def versioned_docs_url(self) -> str | None:


class Application:
"""Craft Application Builder.
"""The Application itself.

This class orchestrates the running of the application.

:ivar app: Metadata about this application
:ivar services: A ServiceFactory for this application
Expand Down
13 changes: 10 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,18 @@
"github_url": "https://github.com/canonical/craft-application",
}

extensions = [
"canonical_sphinx",
]
extensions = ["canonical_sphinx", "sphinx.ext.autodoc", "sphinx.ext.intersphinx"]
# endregion

autodoc_typehints_format = "short"
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"craft-cli": (
"https://canonical-craft-cli.readthedocs-hosted.com/en/latest/",
None,
),
}

# region Options for extensions
# Github config
github_username = "canonical"
Expand Down
83 changes: 83 additions & 0 deletions docs/reference/application.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.. py:currentmodule:: craft_application.application

The Application
===============

The :py:class:`Application` class is the main entry point to your craft-application
application. Ideally, your application's ``main()`` method would simply look something
like:

.. code-block:: python

def main() -> int:
"""Run witchcraft."""
register_services() # register any custom services.
app = craft_application.Application(
app=APP_METADATA,
services=craft_application.ServiceFactory(app=APP_METADATA)
)
return app.run()

In practice, you may need to subclass ``Application`` in order to provide custom
functionality.

Startup process
---------------

The startup process for a craft application can be broken into the following steps that
kick off when running the :py:meth:`Application.run` method.

1. Set up logging. Here :external+craft-cli:doc:`craft-cli <index>` is configured and
any relevant loggers are added to the emitter. This set of loggers can be extended
using the ``extra_loggers`` parameter when instantiating the ``Application``.
#. Load any application plugins. Any module that is configured as a craft-application
plugin is loaded at this point and configured.
#. Start up ``craft-parts``. This activates any relevant features and registers
default plugins.
#. Create the craft-cli :external+craft-cli:class:`~craft_cli.Dispatcher`.
#. Configure the application based on extra global arguments.
#. Load the command
#. Set the project directory.
#. Determine the fetch service policy.
#. Determine whether the command should run in managed mode
#. Determine whether the command needs a project.
#. Configure the services
#. Run the command class.

At this point, run control is handed to the ``Command`` class, which has access to
the application metadata and the service factory. The only remaining responsibility
of the ``Application`` is error handling.

Error handling
--------------

The ``Application`` takes care of most errors raised by commands. In general, these
errors fall into two categories:

- Craft errors: Any error that matches the ``CraftError`` protocol.
- Internal errors: Any other child class of :external+python:class:`Exception`

Craft errors are treated as user errors. They are presented to the user in the amount
of detail presented by craft-cli, including documentation links and whether or not
the log location is shown.

Internal errors, on the other hand, are treated as errors with the application. If
a command raises (or passes through) an Exception, the ``Application`` alerts the
user of an internal error and includes the log path. Almost any internal error can be
considered to be a bug. If the error is due to erroneous user input or other
circumstances beyond the control of either the application or craft-application, the
bug is that the error was not properly converted to a Craft error. These should be
reported to the application and, if relevant, raised to craft-application on
`our own issue tracker <https://github.com/canonical/craft-application/issues>`_.

``KeyboardInterrupts`` are also handled, in this case to silence the stack trace
that Python raises and exit with the common exit code ``130`` (``128 + SIGINT``).

API documentation
-----------------

.. autoclass:: AppMetadata
:members:

.. autoclass:: Application
:members:
2 changes: 1 addition & 1 deletion docs/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Application
===========

- Add a feature to allow `Python plugins
https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/>`_
<https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/>`_
to extend or modify the behaviour of applications that use craft-application as a
framework. The plugin packages must be installed in the same virtual environment
as the application.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Reference
:maxdepth: 1

changelog
application
environment-variables
platforms

Expand Down
Loading