Create a new plugin¶
This is a tutorial to create a pmakeup plugin.
For an example of a plugin, you can view the archive-pmakeup-plugin, available here <https://github.com/Koldar/pmakeup/tree/main/plugins/archive-pmakeup-plugin>.
Determine the name of the plugin¶
pmakeup can automatically load plugins only if they are named in either one of the following pattern:
r”^pmakeup-plugin(s)?-.+”;
r”.+-pmakeup-plugin(s)?$”;
At the beginning of the run of pmakeup, the software will first scan all the installed packages. When it finds a plugin with the specified name, it will further explores it. So, if you want to develop a plugin is required to follow that patterns.
Structure of a pmakeup plugin¶
Generally speaking, a pmakeup plugin should have the specified structure:
myfoobar-pmakeup-plugin/ (<-- this is just the plugin root folder)
myfoobar_pmakeup_plugin/
__init__.py
MyFoobarPMakeupPlugin.py
version.py
tests/
test.py
README.md
LICENSE.md
requirements.txt
setup.py
You can also use pmakeup
to automatically build and deploy your plugin on pypi, but this is another story. The example here
specifies a setuptools
installation way. pmakeup
relies on egg-infos. We first look at the file top_level.txt
in order to fetch the main package name. Then we gain access to such a package and read the __init__.py
.
Finally, we scan whatever __init__.py
has loaded. If it finds a class which derives from pm.AbstractPMakeupPlugin
it is autoamtically added in the pmakeup
graph.
for apackage in map(lambda p: p, pkg_resources.working_set):
package: "EggInfoDistribution" = apackage
if not is_package_name_compliant_with_pmakeup_plugin_name(package.project_name):
continue
# get top level file
top_level_file = os.path.join(package.egg_info, "top_level.txt")
main_package = read_top_level_file(top_level_file)
module_path = os.path.join(package.location, main_package, "__init__.py")
module = import_module(module_path, main_package)
# fetch plugins
for candidate_classname in dir(module):
candidate_class = getattr(module_instance, candidate_classname)
if not inspect.isclass(candidate_class):
continue
if issubclass(candidate_class, pm.AbstractPmakeupPlugin):
result.append(candidate_class)
Now let’s see what the files should contain.
version.py¶
This file is easy, it is the version of the package. It should contain one line with a variable set to a semantic version 2 compliant string:
VERSION = "1.0.4"
__init__.py¶
This file is very easy as well. It should import all the pmakeup
plugin classes that you want to export to pmakeup
.
For instance, we will export just one plugin:
from myfoobar_pmakeup_plugin.MyFoobarPMakeupPlugin import MyFoobarPMakeupPlugin
MyFoobarPMakeupPlugin.py¶
This file should contain a class that implements pm.AbstractPMakeupPlugin
:
import pmakeup as pm
class MyFoobarPMakeupPlugin(pm.AbstractPMakeupPlugin):
def _setup_plugin(self):
pass
def _teardown_plugin(self):
pass
def _get_dependencies(self) -> Iterable[type]:
return []
@pm.register_command.add("really_important")
def say_hello(self, name: str) -> bool:
"""
Say hello to everyone
"""
self.logs.echo(f"Hello {name}!")
return True
If you don’t need that another plugin _setup_plugin
method is called before this one, you can leave _get_dependencies
to []
.
setup and teardown methods are called whenever the plugin is initialized and finalized.
Any function that you want to call in a pmakeup
script needs to be decorated with @pm.register_command.add
decorator:
the string can be whatever you want, it is used only for grouping the functions together.
If you need to gain access to other plugins, you can use self.get_plugin(<plugin_name>)
to gain access to the corresponding
plugin instance. pmakeup
automatically loads some really core plugnis and it provides a property in AbstractPMakeupPlugin
:
for example self.logs
is used to print something to the console.
setup.py¶
Just for completeness, this is the setup.py
that I use to build a plugin:
import os
from typing import Iterable
import setuptools
from archive_pmakeup_plugin import version
PACKAGE_NAME = "archive-pmakeup-plugin"
PACKAGE_VERSION = version.VERSION
PACKAGE_DESCRIPTION = "A Pmakeup plugin for handling zip and unzip operations"
PACKAGE_URL = "https://github.com/Koldar/pmakeup.git"
PACKAGE_PYTHON_COMPLIANCE = ">=3.6"
PACKAGE_CLASSIFIERS = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
AUTHOR_NAME = "Massimo Bono"
AUTHOR_EMAIL = "massimobono1@gmail.com"
#########################################################
# INTERNALS
#########################################################
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
def get_dependencies(domain: str = None) -> Iterable[str]:
if domain is None:
filename = "requirements.txt"
else:
filename = f"requirements-{domain}.txt"
if os.path.exists(filename):
with open(filename, "r", encoding="utf-8") as fh:
dep = fh.readline()
dep_name = dep.split("==")[0]
yield dep_name + ">=" + dep.split("==")[1]
setuptools.setup(
name=PACKAGE_NAME,
version=PACKAGE_VERSION,
author=AUTHOR_NAME,
author_email=AUTHOR_EMAIL,
description=PACKAGE_DESCRIPTION,
long_description=long_description,
long_description_content_type="text/markdown",
license_files="LICEN[SC]E*",
url=PACKAGE_URL,
packages=setuptools.find_packages(),
classifiers=PACKAGE_CLASSIFIERS,
install_requires=list(get_dependencies()),
extras_require={
"test": list(get_dependencies("test")),
"doc": list(get_dependencies("doc")),
},
include_package_data=True,
package_data={
"": ["package_data/*.*"],
},
python_requires=PACKAGE_PYTHON_COMPLIANCE,
)