This chapter describes how to write custom module-extensions for teapot.
We already seen in Filters what a filter is. Now is the time to write your owns !
A filter is a simple function that takes no parameters and returns a boolean value.
To register a new filter, use the teapot.filters.decorators.named_filter() decorator.
Registers a function to be a filter.
Registers the function with the specified name.
If another function was registered with the same name, a DuplicateFilterError will be raised, unless override is truthy.
If a filter depends on other filters, those will be checked before the actual filter gets run.
Here is an example of some built-in filters:
import os
import sys
from teapot.filters.decorators import named_filter
@named_filter('windows')
def windows():
"""
Check if the platform is windows.
"""
return sys.platform.startswith('win32')
@named_filter('msvc', depends='windows')
def msvc():
"""
Check if MSVC is available.
"""
return 'VCINSTALLDIR' in os.environ
As seen in Extensions, an extension is a function, that always takes a builder argument, optionally takes string parameters and returns a string.
Extensions are to teapot what macros are to the C language.
To register a new extension, use the teapot.extensions.decorators.named_extension() decorator.
Here is an example of some built-in extensions:
import os
import sys
from teapot.path import windows_to_unix_path
from teapot.extensions.decorators import named_extension
@named_extension('prefix')
def prefix(builder, style='default'):
"""
Get the builder prefix.
"""
result = os.path.join(builder.attendee.party.prefix, builder.attendee.prefix, builder.prefix)
if sys.platform.startswith('win32') and style == 'unix':
result = windows_to_unix_path(result)
return result
Note that the function must always have first builder argument that will be valued with the current teapot.builders.Builder instance.
Fetchers are responsible for downloading or copying the source archives from a specified location.
To define a new fetcher, just derive from teapot.fetchers.base_fetcher.BaseFetcher.
Here is an example with the built-in file fetcher:
import os
import shutil
import mimetypes
from teapot.fetchers.base_fetcher import BaseFetcher
class FileFetcher(BaseFetcher):
"""
Fetchs a file on the local filesystem.
"""
shortname = 'file'
def read_source(self, source):
"""
Checks that the `source` is a local filename.
"""
if os.path.isfile(source.location):
self.file_path = os.path.abspath(source.location)
return True
def do_fetch(self, target):
"""
Fetch a filename.
"""
archive_path = os.path.join(target, os.path.basename(self.file_path))
archive_type = mimetypes.guess_type(self.file_path)
size = os.path.getsize(self.file_path)
self.progress.on_start(target=os.path.basename(archive_path), size=size)
shutil.copyfile(self.file_path, archive_path)
# No real interactive progress to show here.
#
# This could be fixed though.
self.progress.on_update(progress=size)
self.progress.on_finish()
return {
'archive_path': archive_path,
'archive_type': archive_type,
}
All callback classes derive from teapot.fetchers.callbacks.BaseFetcherCallback.
Unpackers are responsible for extracting the content of the source archives into an exploitable source tree.
To define a new unpacker, just derive from teapot.unpackers.base_unpacker.BaseUnpacker.
Base class for all unpacker classes.
If you subclass this class, you will have to re-implement the do_unpack() method to provide your specific unpacker logic.
If you desire to attach your unpacker to certain mimetypes, please do so by defining the class-level member types that must be a list of couples (mimetype, encoding).
Unpack an archive.
The archive to unpack can be reached at self.archive_path.
It must raise an exception on error.
You can provide feedback on the unpacking operation by calling self.progress.on_start, self.progress.on_update and self.progress.on_finish at the appropriate time.
See teapot.unpackers.callbacks.BaseUnpackerCallback for further details.
Here is an example with the built-in tarball unpacker:
from teapot.unpackers.base_unpacker import BaseUnpacker
import os
import tarfile
class TarballUnpacker(BaseUnpacker):
"""
An unpacker class that deals with .tgz files.
"""
types = [
('application/x-gzip', None),
('application/x-bzip2', None),
]
def do_unpack(self):
"""
Uncompress the archive.
Return the path of the extracted folder.
"""
if not tarfile.is_tarfile(self.archive_path):
raise InvalidTarballError(archive_path=self.archive_path)
tar = tarfile.open(self.archive_path, 'r')
# We get the common prefix for all archive members.
prefix = os.path.commonprefix(tar.getnames())
# An archive member with the prefix as a name should exist in the archive.
while True:
try:
prefix_member = tar.getmember(prefix)
if prefix_member.isdir:
break
except KeyError:
pass
new_prefix = os.path.dirname(prefix)
if prefix == new_prefix:
raise TarballHasNoCommonPrefixError(archive_path=self.archive_path)
else:
prefix = new_prefix
source_tree_path = os.path.join(self.attendee.build_path, prefix_member.name)
self.progress.on_start(count=len(tar.getmembers()))
for index, member in enumerate(tar.getmembers()):
if os.path.isabs(member.name):
raise ValueError('Refusing to extract archive that contains absolute filenames.')
self.progress.on_update(current_file=member.name, progress=index)
tar.extract(member, path=self.attendee.build_path)
self.progress.on_finish()
return {
'source_tree_path': source_tree_path,
}
All callback classes derive from teapot.unpackers.callbacks.BaseUnpackerCallback.
post-actions are simple function that takes a teapot.party.Party instance as a parameter, and that gets executed right after a such instance was initialized.
You can register a post-action using the teapot.party.Party.register_post_action() decorator.
Here is an example a built-in post-action that registers the defaut environment:
@Party.register_post_action
def add_default_environment(party):
"""
Add the default environment to the party.
"""
party.environment_register.register_environment(DEFAULT_ENVIRONMENT_NAME, create_default_environment())