Develop a Type Extension
A type extension defines and adds a new type <definition_type> to the object model. For this new type to be usable it must be accessible to the user. Therefore, it is sensible for the extension to also provide three further components, namely a user interface Type View and a factory (Type View Factory) that can produce them on demand, and optionally some Actions.
Developing a type extension is relatively simple. In this walk-through we develop a Type, a custom Type View <type_view> and an Action <definition_action> for it.
Note
This walk-through re-implements the File
type already provided by the Base
extension. The upstream source code can be found in extensions/base
.
Defining the Extension Entrypoint
A valid extension is required to provide an entrypoint. Currently this means that the extension must provide a special functions called create
which takes a single parameter of type Extension Api. This function is called when the extension is loaded.
For more information on how to create the entrypoint, see the documentation on Extension Development.
Creating and Loading a Type Definition
The first file that needs to be created is the Type definition file. This file is a JSON file that contains the definition of the Type and should be named <type_name>.json
. In the case of a File
type it could look like this:
{
"super": { "$ref": "bftype://Map" },
"type": "File",
"required": [
"file_path",
"file_type"
],
"properties": {
"file_path": {
"super": null,
"type": "String"
},
"file_type": {
"super": null,
"type": "String"
}
}
}
The Type definition file contains the following information:
The super class of the Type. This is the Type that our Type extends. In this case the File Type extends the Map Type. Note, that this is a reference with the
bftype://
protocol. This is a reference to a Type that must already be loaded in the Type Registry.The name of the Type. This is the name that will be used to reference the Type. In this case the Type is called
File
.Required properties. These are the properties that are required to be set on instances of this type. In this case the
file_path
andfile_type
properties are required.The properties themselves. These are the properties that are available on the type instance. In this case the
file_path
andfile_type
properties are available.The
file_path
property is aString
Type.The
file_type
property is aString
Type.
With this we can extend our Extension to register the File Type into the Type Registry:
from blackfennec.extension_system import Extension
from blackfennec.extension_system import ExtensionApi
class MyExtension(Extension):
def __init__(self, api: ExtensionApi):
super().__init__(
name='My Extension',
api=api)
self._file_type = None
self._action = None
def register_types(self):
# currently the type loader does also register the type
self._file_type = self._api.type_loader.load('file/file.json')
def deregister_types(self):
self._api.type_registry.deregister_type(self._file_type)
self._file_type = None
If we didn’t want to add any special functionality for our new Type we could stop here.
Creating a Wrapper for the Type
Black Fennec lacks the ability to create a concrete Type instance directly. Instead it is recommended to create a wrapper for the Type that can be used to interact with instances of it. The snippet below is an example of a File wrapper:
class File:
"""File type wrapper
Helper class representing an instance of a 'File'.
Can be used by other classes as a helper interact with the underlay more easily.
"""
FILE_PATH_KEY = 'file_path'
FILE_TYPE_KEY = 'file_type'
def __init__(self, subject: Map = None):
"""File Constructor
Args:
subject (Map): underlying map interpretation to
which property calls are dispatched
"""
self._subject: Map = subject or Map()
if File.FILE_PATH_KEY not in self._subject.value:
self._subject.add_item(File.FILE_PATH_KEY, String())
if File.FILE_TYPE_KEY not in self._subject.value:
self._subject.add_item(File.FILE_TYPE_KEY, String())
@property
def subject(self):
return self._subject
def _get_value(self, key):
if key not in self.subject.value:
return None
return self.subject.value[key].value
def _set_value(self, key, value):
assert key in self.subject.value
self.subject.value[key].value = value
@property
def file_path(self) -> str:
return self._get_value(File.FILE_PATH_KEY)
@file_path.setter
def file_path(self, value: str):
self._set_value(File.FILE_PATH_KEY, value)
@property
def file_type(self) -> str:
return self._get_value(File.FILE_TYPE_KEY)
@file_type.setter
def file_type(self, value: str):
self._set_value(File.FILE_TYPE_KEY, value)
Creating the View Model
Next we want to create a view model.
Note
We recommend using MVVM.
class FileViewModel:
"""View model for core type File."""
def __init__(self, interpretation: Interpretation):
"""Create constructor
Args:
interpretation (Interpretation): The overarching
interpretation
"""
self._interpretation = interpretation
self._file: File = File(interpretation.structure)
@property
def file_path(self):
"""Property for file path"""
return self._file.file_path
@file_path.setter
def file_path(self, value: str):
self._file.file_path = value
@property
def file_type(self):
"""Property for file type"""
return self._file.file_type
@file_type.setter
def file_type(self, value: str):
self._file.file_type = value
def navigate(self):
self._interpretation.navigate(self._interpretation.structure)
Creating the View
This file depends on how one wants to visualize the Type. Important is that your view is as responsive as possible, as you never know how big a presenter will show your Type View. For an example please see the FileView in the Base Extension
package located in extensions/base/base/file
.
Writing a ViewFactory
Creating a view is a non-trivial problem. This is why Black Fennec does not create them itself. Instead you have to register a ViewFactory capable of creating a view for your Type.
Luckily creating a view for a File is rather simple. First, we create the view model and after we can construct the appropriate view.
class FileViewFactory:
"""Creator of the FileView"""
def satisfies(self, specification: Specification) -> bool:
"""Test if this view factory can satisfy the specification
Args:
specification (Specification): the specification to be satisfied
Returns:
bool: True if the specification can be satisfied. Otherwise False.
"""
return True
def create(self, interpretation: Interpretation) -> FileView:
"""creates a FileView
Args:
interpretation (Interpretation): The overarching
interpretation.
specification (Specification): The specification which can fine
tune the creation function.
Returns:
FileView
"""
view_model = FileViewModel(interpretation)
if interpretation.specification.is_request_for_preview:
return FilePreview(view_model)
return FileView(view_model)
Registering the View
The last step is to register the view for the File Type. This can be done by adding the following code to the create_extension method:
class MyExtension(Extension):
# ...
def register_view_factories(self):
self._api.view_registry.register_view_factory(
self._file_type,
Specification(),
FileViewFactory())
def deregister_view_factories(self):
self._api.view_registry.deregister_type_view_factory(
self._file_type,
Specification())
Creating an Action
An Action always registers itself to a Type. In return we are guaranteed to receive an instance of this type when the action is triggered. We will register our action to the File type.
The definition of an action is again relatively simple. We only need to implement a method called execute
and two properties describing the action.
from blackfennec.action_system.action import Action
from blackfennec.action_system.context import Context
class GuessMimeTypeAction(Action):
def __init__(self,
map_type,
resource_type_registry: ResourceTypeRegistry):
super().__init__(map_type)
self._resource_type_registry: ResourceTypeRegistry = \
resource_type_registry
def execute(self, context: Context):
file_type = File(context.structure)
resource_type_id = ResourceType.try_determine_resource_type(
file_type.file_path)
resource_type = self._resource_type_registry.resource_types[
resource_type_id
]
mime_type = MimeType.try_determine_mime_type(
file_type.file_path,
resource_type)
file_type.file_type = mime_type
@property
def name(self):
return "guess mime type"
@property
def description(self):
return """Tries to determine the mime type of a file."""
The execute
method is called when the action is triggered. The context
parameter contains the instance of the type the action is registered to but we have to “cast” it with our wrapper. In our case this is a File
instance. We can use this instance to access the file path and to set the mime type.
Finally, we need to register the action to the File
type. This is done in the register_actions
method.
class MyExtension(Extension):
# ...
def register_actions(self):
self._action = DetectMimeTypeAction(self._file_type)
self._api.action_registry.register_action(self._action)
def deregister_actions(self):
self._api.action_registry.deregister_action(self._action)
self._action = None
Conclusion
In conclusion, the Black Fennec extension system allows developers to easily add custom functionality to the application through the creation of type extensions. By following the steps outlined in this tutorial, developers can create a new type, create a custom user interface for that type, and add actions to manipulate instances of that type. The extension system provides a clear and intuitive way to extend Black Fennec and tailor it to specific needs and use cases.