diff --git a/craft_application/application.py b/craft_application/application.py index c92b5d154..444c6db19 100644 --- a/craft_application/application.py +++ b/craft_application/application.py @@ -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 .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: @@ -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 diff --git a/docs/conf.py b/docs/conf.py index 527df7a9b..f73a6136b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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" diff --git a/docs/reference/application.rst b/docs/reference/application.rst new file mode 100644 index 000000000..281a3d84b --- /dev/null +++ b/docs/reference/application.rst @@ -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 ` 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 `_. + +``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: diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst index fc026206d..6b183de43 100644 --- a/docs/reference/changelog.rst +++ b/docs/reference/changelog.rst @@ -23,7 +23,7 @@ Application =========== - Add a feature to allow `Python 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. diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 1e7796c63..3672962b4 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -7,6 +7,7 @@ Reference :maxdepth: 1 changelog + application environment-variables platforms