Basic sample#
To demonstrate the composition usage we will create a dummy project which should be able to collect various messages from project applications and print them out in the expected order.
The final result of this tutorial can be found in its own source repository composer-sveetch-python.
Starting project#
This document will use virtualenv and pip, although you are free to use your preferred tools.
First create the project directory where to work:
mkdir composer-sample
cd composer-sample
virtualenv -p python3 .venv
source .venv/bin/activate
pip install project-composer
Creating project manifest#
The first required step is to create the project manifest to configure how the composer will work in your project.
Note
This sample use the TOML file format but it could be also in a JSON file format.
In fact you could also directly use a Python dictionnary but it would work only programatically and you would lose usage of included Composer commandline scripts which only works with manifest files.
Open your editor and create a file pyproject.toml
with following content:
[project]
name = "Sample"
[tool.project_composer]
collection = [
"ping",
"bar",
"foo",
]
repository = "application_repository"
For this sample we just focus on the required parameters.
Create application repository#
As you can see from the manifest, we need a repository directory named
application_repository
that will contains application directories bar
, foo
and ping
.
Finally you will end with the following structure:
application_repository/
├── bar/
│ ├── __init__.py
│ └── messages.py
├── foo/
│ ├── __init__.py
│ └── messages.py
└── ping/
├── __init__.py
└── messages.py
Now enter in the application_repository
directory to create your applications.
The ‘foo’ application#
This will be the lowest level application because it does not depend on any other one.
Since it does not have any dependency just create an empty file at
foo/__init__.py
.
Then we will create the messager part at foo/messages.py
:
from project_composer.marker import EnabledApplicationMarker
class FooFirstMessage(EnabledApplicationMarker):
def load(self):
messages = super().load()
messages.append("Foo first")
return messages
class FooSecondMessage(EnabledApplicationMarker):
def load(self):
messages = super().load()
messages.append("Foo second")
return messages
As you can see, every message classes inherit from EnabledApplicationMarker
which
is just a marker class, it does not implement anything and is just used by composer
to recognize the classes it have to register. Any other class without this marker will
be ignored.
So here we created two messager classes, each one implement the load()
method to
append a new message in a list. This method will be used by message collector class to
collect messages from all enabled applications, we will see further about message
collector.
Note
When there is multiple enabled classes in the same module, they will be collected and ordered in the natural order they are defined in their module. Not any special sortering is applied.
This is pretty basic but you can implement almost everything you need for your specific content collector because finally the composer just care about collection, your final collector will just be a class composed from application classes that you will be free to use how you need.
The ‘bar’ application#
This application will depends on foo
application. In resume for the composer it
will says “‘bar’ depends from ‘foo’ so it must be loaded after ‘foo’”.
Dependency definitions are done in the application base module, so create a file at
bar/__init__.py
with this:
DEPENDENCIES = [
"foo"
]
The dependencies are defined in a simple list with their application name. Obviously a dependency name must exists in your manifest collection since composer must know it to follow the full dependency tree.
Note
The order of applications in collection is not really important since composer will resolve the right order from dependencies.
However the order of application dependencies have some influences on final order resolving.
Then we will create the messager part at bar/messages.py
:
from project_composer.marker import EnabledApplicationMarker
class BarMessage(EnabledApplicationMarker):
def load(self):
messages = super().load()
messages.append("Bar")
return messages
This is alike the foo
messager part except it only define a single messager.
The ‘ping’ application#
And the last application which is almost identical to bar
. It depends from bar
so it inherits from its dependencies and indirectly depends from foo
. Composer
will order it after foo
and bar
.
Now so create a file at ping/__init__.py
to define its direct dependencies:
DEPENDENCIES = [
"bar"
]
Note
An application only needs to define its direct dependencies that means only the applications it directly requires. When composer perform order resolving will walk in dependency dependencies and further, so no need to define the whole dependency tree.
Then we will create the messager part at ping/messages.py
:
from project_composer.marker import EnabledApplicationMarker
class PingMessage(EnabledApplicationMarker):
def load(self):
messages = super().load()
messages.append("Ping")
return messages
Composition usage#
Now that we got the Manifest and the repository, we can start to use composition.
Get back to the parent directory and create a new file at hello.py
, everything now
will go in this script file.
Import composition stuff#
We will start it with the required import from composer and Path object. We need the composer itself and the used processor to get enabled classes from application message modules:
from pathlib import Path
from project_composer.compose import Composer
from project_composer.processors import ClassProcessor
Messager#
To demonstrate the result of composition, we implement a basic message collector, append this to the script:
class MessagerBase:
"""
Application messages collector
"""
def load(self):
return []
def get_messages(self):
output = ""
messages = self.load()
output = "\n".join([
"- Hello {}".format(m) for m in messages
])
return output
As you can see this is something with higher level than composer, it even does not relate to anything from composer.
This collector will be combined with registered messager classes from applications, it
will be the top of the messager classes hierarchy so its load()
method just setup
a empty list that messager classes will fill each one after ones.
Its get_messages()
method it just a shortand to format the message list. Finally
we just want to output a line starting with Hello
followed by a single message for
each message.
Message processor#
Now we will create the processor dedicated to find available message classes from enabled applications, append this to the script:
class MessageProcessor(ClassProcessor):
"""
Processor for enabled application settings classes for a Django project.
"""
def get_module_path(self, name):
"""
Return a Python path for a module name.
Arguments:
name (string): Module name.
Returns:
string: Python path from repository to application module.
"""
return "{base}.{part}".format(
base=self.composer.get_application_base_module_path(name),
part="messages",
)
It inherits from ClassProcessor
since this processor only look for Python classes.
Note
The only purpose of a processor is to find available content like Python classes or content files. This is not the goal of a processor to perform anything about retrieved content.
This is because processors are only used by composer to resolve application hierarchy and build application parts composition. And so a processor should be free of any dependency or related code, excepting the ones from composer.
As you can see in this example the only thing to implement is the get_module_path
method which build the right Python path to search application part modules. Here we
are looking for a messages
module in applications, so for our sample repository it
will match foo.messages
, bar.messages
and ping.messages
paths.
Use composed class#
Everything is ready we just have to glue them and get results.
Let’s start to initialize the composer:
# Initialize composer with the manifest and the message processor
_composer = Composer(Path("./pyproject.toml").resolve(),
processors=[MessageProcessor],
)
Then proceed to resolve the application order depending their dependencies:
# Resolve dependency order
_composer.resolve_collection(lazy=False)
And tell the composer to get message classes from enabled applications:
# Search for all enabled message classes
_classes = _composer.call_processor("MessageProcessor", "export")
At this point the composer is ready, we can start to inspect what’s going on.
Let’s check the application collection as defined from manifest:
print("collection:", _composer.manifest.collection)
Running the script should return the collection list as defined from manifest, its order have not changed:
$python hello.py
collection: ['ping', 'bar', 'foo']
Now add the following code to the script to check for the resolved application list ordered after dependency hierarchy:
print("apps:", _composer.apps)
Running the script should now output the application list in the right order:
apps: [<AppNode: foo>, <AppNode: bar>, <AppNode: ping>]
As you see the resolved application list is not anymore just name strings but
AppNode
objects and most important the order has changed as expected from defined
application dependencies.
And for the last inspection, we will see what message classes have been retrieved from processor, add the following to the script:
print("_classes:", [cls.__name__ for cls in _classes])
Running the script should now output the class list ordered after the resolved application order:
_classes: ['FooFirstMessage', 'FooSecondMessage', 'BarMessage', 'PingMessage']
Enough of inspection, we will finish this script. First we build the final messager class:
# Reverse the list since Python class order is from the last to the first
_classes.reverse()
# Add the base messager as the base inheritance
_COMPOSED_CLASSES = _classes + [MessagerBase]
# Compose the final messager from found classes
Messager = type(
"Messager",
tuple(_COMPOSED_CLASSES),
{}
)
We reverse the class list since Python class inheritance goes from the last to the
first class, then add the MessagerBase
at the end so it is processed first and
finally we build the class with type
using the classes list.
And to finish, we append the lines to exploit this class and print its output:
# Use messager to collect all messages in the right order
messager = Messager()
messages = messager.get_messages()
# And finally output all collected messages
print()
print(messages)
Running the script should now output every messages in the right order:
- Hello Foo first
- Hello Foo second
- Hello Bar
- Hello Ping
Conclusion#
Because Project composer want to be flexible there is no real shortand to perform composition in a single line and you will need a little dozen to achieve it.
But there is no magic behind this and you should be able to integrate it everywhere.
Finally this sample is pretty basic and did not mention some advanced features.