Interfaces and Handlers
Last updated
Last updated
Cement builds upon a standard interface and handler system that is used extensively to break up pieces of the framework into relatable chunks, and allow customization of everything from logging to config file parsing, and almost every operation in between.
In Cement, an interface is what defines some functionality, and a handler is what implements that functionality.
We call the implementation of an interface a handler, and provide the ability to easily register and retrieve them via the app.handler
object. Cement interfaces are defined as Python Abstract Base Classes, and handlers implement them by sub-classing and overriding the defined abstract methods required to make the implementation legit.
API References
The following interfaces are builtin to Cement's core foundation:
The InterfaceManager (app.interface
) provides quick mechanisms to list, get, and verify interfaces:
Defining interfaces is more of an advanced topic, and is not required to fully grasp for new developers or those new to the framework.
Cement uses interfaces and handlers extensively to manage the framework, however developers can also make use of this system to provide a clean, and standardized way of allowing other developers to customize their application (generally via application plugins).
The following defines a basic interface we'll call greeting
:
The above example defines the greeting
interface, by providing abstract methods that any handlers implementing this interface must provide. It does not implement any functionality on its own (though it could), but rather defines and documents its purpose and its expected implementation. The interface is easily defined with the framework by listing it in App.Meta.interfaces
, but you can also define interfaces directly with app.interface.define()
.
In order to implement the above greeting
interface, we first want to provide a handler base class that will be the starting point for all implementations that sub-class from it:
Handler base classes sub-class from both the interface they are implementing (GreetingInterface
), and also the Cement Handler base class (Handler
). The application developer defining the interface should always provide a handler base class for the implementation, even if the base class does not fully satisfy the interface.
In the above example, the developer would call the greet()
method that will do all of the common operations like logging, exception handling, etc for all implementations, leaving only the minimal _get_greeting()
method to be provided by the final implementation sub-classes. This follows the common Don't Repeat Yourself (DRY) principle's best practice (all-be-it a tediously simple example), where all of the reusable logic can live in one place, and sub-classes only focus on their unique means of implementing an interface.
In order to complete our implementation of the above greeting
interface, we can now sub-class from the provided GreetingHandler
base class, and fill in the missing pieces:
An interface defines itself to the framework via the Interface.Meta.interface
string, and a handler defines itself via the Handler.Meta.label
string. Collectively, all implementation handlers are referred to by both as <interface>.<label>
or in the above examples greeting.hello
and greeting.goodbye
.
The following is an MCVE of defining an interface, providing an implementation handler base class, and registering multiple handlers that implement the interface differently:
Where an interface defines what an implementation is expected to provide, a handler is what actually implements the interface.
The HandlerManager (app.handler
) provides quick mechanisms to list, get, resolve and verify handlers. The following are a few examples of working with handlers:
It is important to note that handlers are stored within the application as uninstantiated objects. Meaning you must instantiate them after retrieval (call _setup(app)
) or simply pass setup=True
to the app.handler.get()
method.
An interface simply defines what an implementation is expected to provide, whereas a handler actually implements the interface.
The following is a simple example of sub-classing an existing handler, then registering that with the framework.
Cement sets up a number of default handlers for logging, config parsing, etc. These can be overridden in a number of ways, however the primary method is to override their associated setting in App.Meta.
Using the above MyConfigHandler
example, we simply need to register a handler to an interface, and then override the App.Meta.config_handler
option:
All builtin core interfaces have an associated App.Meta.x_handler
option, allowing complete customization of every aspect of the framework. See the reference documentation of App.Meta for more detail.
All handlers and interfaces are unique. In most cases, where the framework is concerned, only one handler is used. For example, whatever is configured for the App.Meta.log_handler
will be used and setup as app.log
. However, take for example an Output Handler. You might have a default App.Meta.output_handler
of mustache
(a text templating language) but may also want to override that handler with the json
output handler when -o json
is passed at command line. In order to allow this functionality, both the mustache
and json
output handlers must be registered.
Any number of handlers can be registered to an interface. You might have a use case for an Interface/Handler that may provide different compatibility based on the operating system, or perhaps based on simply how the application is called. A good example would be an application that automates building packages for Linux distributions. An interface would define what a build handler needs to provide, but the build handler would be different based on the OS. The application might have an rpm
build handler, or a dpkg
build handler to perform the build process differently.
Depending on the handler, you will have different ways of customizing its functionality. Some handlers honor application configuration setting, while others may only rely on meta-options. In either case, both can be modified at the top level of your application meta.
In the following example, we modify the configuration defaults of the log handler, and also the meta-options to enable an optional log level command line argument feature it supports.
Note that configuration settings are always overridable via configuration files, however this example is modifying the builtin default setting of the log handler.
If modifying the configuration or meta options isn't enough, you can always sub-class an existing handler and register your own in its place.
In some use cases, you will want the end user to have access to override the default handler of a particular interface. For example, Cement ships with multiple Output Handlers including json
, yaml
, and mustache
. A typical application might default to using mustache
to render console output from text templates. That said, without changing any code in the application, the end user could simply pass the -o json
command line option and output the same data that is rendered to template, out in pure JSON.
Output hander overrides are not enabled by default, but can be enabled by setting the OutputHandler.Meta.overridable
option to True
for the output handlers that you want overridable by the -o
option at command line. See the documentation on Output Rendering for more details and examples.
The only built-in handler override that Cement includes is for the above mentioned JSON example, but you can add any that your application requires.
Notice the -o
command line option, that includes the choices: yaml
and json
. This feature will include all Output Handlers that have the overridable
meta-data option set to True
. We did not set this option for the mustache handler, therefore it did not show up as a choice for the -o
option at command-line.
Interface
Description
Framework extension loading.
Messaging to console, and/or file via common log facilities (INFO, WARNING, ERROR, FATAL, DEBUG).
Merging of application configuration defaults, configuration files, and environment settings into a single config object.
Remote message sending (email, smtp, etc).
Application plugin loading.
Rendering of template data (content, files, etc).
Rendering of data/content to end-user output (console text from template, JSON, Yaml, etc). Often uses an associated template handler backend.
Command line argument/option parsing.
Command dispatch (sub-commands, arguments, etc)
Key/Value data store (memcached, redis, etc)