# Addon System
This is a fully rewritten RelativeAddonsSystem
[PyPi]
[GitHub]
What is it?
This is super useful (or useless. Depends on your mood) thing ever!
This is a library that allows you
to manage "addons" in your project.
You want to install only one library?
Just do it!
It has no third-party dependencies!
What is addon?
Beneficial thing!
Addon is a mini-project(usually independent)
that provides an interface for your main project
and can be used as a part of it.
Addon is a directory that contains at least two files:
- Addon metafile
- Addon main module
Addon metafile
Is a JSON formatted file. That consists of these values:
- id -
string
. Addons must have unique id. Usually
consists of your username and addon name: KuyuGama/SomeAddon
- name -
string
. Name of addon.
Can be used for frontend display - module -
string
. Main module of addon - authors -
array[string]
. Author names
of addon - version - [optional]
string
. Version of addon(usually SemVer) - description - [optional]
string
. Description of addon.
Can be used on frontend - depends - [optional]
array[string]
. Addon dependencies. Format is library==version
(such as pip). If you are using your own
library managing class, you can change
a string format to yours. - extra - [optional]
object
. Addon extra info. Used to store custom values that can be changed in runtime.
Addon module
Is a standard python module!
The only exception is an interface that will use your main code of project.
I've been inspired by a plugins
for Minecraft servers cores such as a
Paper to create this library!
What is addon for?
Speed or convenience? I choose both!
Addon is a runtime extension of your project.
You can write update for your application
and don't care about downtime
(it will not be there!).
Or you can use addons just for creating
extensible projects. For example, telegram bots,
you can add some functionality with
no need to edit the main code of the bot.
Connect the addon!
Dependencies?
Where is aiogram gone?
This library has a built-in tool for
managing addon's dependencies.
So you don't even need to use
command-line to install it using AddonSystem.
Let's try?
LET'S GOO!
Prerequisites
pip install addon-system
- Create addons storage directory, it can have any name and be anywhere inside your project workdir(of course!)
Example:
┌─ KuyuGenesis -- Workdir
└──┌─ addons -- here you go
├─ src
├─ main.py
├─ config.py
└─ KuyuGenesis.conf
Create your first addon
There are two ways to achieve it
- Using library-provided addon creation
command line tool:
make-addon -name "SomeAddon" -a "KuyuGama,KugaYumo" -i "KuguYama/SomeAddon" -m __init__ addons
- Manually(meh) create addon dir and metafile:
┌─ SomeAddon -- AddonDir (CamelCase)
└──┌─ addon.json -- metafile
└─ __init__.py -- module set in metafile(can be any python file)
So you now have this project structure:
┌─ KuyuGenesis -- Workdir
└──┌─ addons
├──┌─ SomeAddon -- Addon's directory
│ └─┌─ addon.json -- metafile
│ └─ __init__.py -- module set in metafile
├─ src
├─ main.py
├─ config.py
└─ KuyuGenesis.conf
Initialize the system
from pathlib import Path
from addon_system import AddonSystem
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
Here we created instance of the AddonSystem
with early created addons root directory
and pip
library manager.
If you use another library manging tool -
write your own implementation of
library manager that uses it (only three methods!).
Querying addons
Search of addons? YES!
For your needs, you can search for the addons(not on the internet! Yet?)
from pathlib import Path
from addon_system import AddonSystem
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
for addon in system.query_addons(name="some", case_insensitivity=True):
print(addon)
Here we queried for addon by its name case-insensitive.
You can query addons by other fields also.
Here are all query parameters:
- author — author name
- name — addon name
- description — description
- enabled — addon status(about this later)
- case_insensitivity — case-insensitive querying
Getting a specific addon
As I wrote above:
Addon's must have unique id
So, if you want to get some exact addon - use its id!
from pathlib import Path
from addon_system import AddonSystem
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
print(addon)
Addon details
What is the use of this addon?
You can get all supported values from metafile using
AddonMeta that is always present in addon as "metadata" attribute:
from pathlib import Path
from addon_system import AddonSystem
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
metadata = addon.metadata
print(metadata.id)
print(metadata.name)
print(metadata.description)
print(metadata.version)
print(metadata.depends)
print(metadata.authors)
print(metadata.module)
Addon metafile has one more field "extra."
Which can be used to store custom values:
from typing import Any
from pathlib import Path
from addon_system import AddonSystem, AddonMetaExtra
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
class Extra(AddonMetaExtra):
__defaults__ = {"handles_events": []}
handles_events: list[str]
priority: int
def validate(self, data: dict[str, Any]) -> bool:
handles_events = data.get("handles_events")
if not isinstance(handles_events, list):
return False
if isinstance(handles_events, list) and (
len(handles_events) != 0
and all(map(lambda e: isinstance(e, str), handles_events))
):
return False
return True
extra = addon.metadata.extra(Extra)
if "event" in extra.handles_events:
print(f"{addon.metadata.id} can handle event \"event\"")
extra.priority = 0
extra.save()
Note: you can read/edit addon extra info without the need to
create your own AddonMetaExtra class subclass.
But it is useful when you have to define types of field or validate
data from extra info
Dependencies!
To manage dependencies, you have to use the simple
interface:
from pathlib import Path
from addon_system import AddonSystem
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
print(addon.metadata.depends)
print("Is dependencies satisfied?", system.check_dependencies(addon))
if not system.check_dependencies(addon):
system.satisfy_dependencies(addon)
print("Auto-installed dependencies")
...
Here we checked dependencies of addon and installed it if necessarily
check_dependencies
and satisfy_dependencies
also can take addon id to manage
Thanks to caching we can call check_dependencies
twice without losing much time
Addon status
Another useful (or useless, according to your purposes) think!
Addon status allows the code to know whether to
load addon into your project
Usage:
from pathlib import Path
from addon_system import AddonSystem
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
if not system.get_addon_enabled(addon):
system.enable_addon("KuyuGama/SomeAddon")
else:
system.disable_addon("KuyuGama/SomeAddon")
for addon in system.query_addons(enabled=True):
print("Enabled addon:", addon)
To remove confusion:
get_addon_enabled
, enable_addon
and disable_addon
all
can take id or instance of addon
Importing addon
Much more interesting, isn't it?
You can import or reload module of addon
where and when you want:
from pathlib import Path
from addon_system import AddonSystem
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
module = addon.module()
event_handlers = {}
module.unpack_handlers(event_handlers)
Note, addon can't be imported without satisfied dependencies.
If dependencies are not satisfied, exception will be raised
Interface of addon module is your own designed
interface for your purposes.
Because the module is a regular python module, you can
create whatever you want
Reload of module is achieved by using the same function with reload=True
argument:
from pathlib import Path
from addon_system import AddonSystem
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
module = addon.module(reload=True)
Value injection
NOTE! That is not safe in case of usage threading
! Because it replaces builtins
(Only for import time)
Why builtins? Because it will work as it is (without any function calls in addon's module)
Note: In future - builtins will no longer be used for this, so i recommend
to use addon_system.utils.resolve_runtime
You can inject values on module initiation, and use it after (will work only for module that set into metafile)
Injection example:
from pathlib import Path
from addon_system import AddonSystem
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
addon.namespace.update(dict(this=addon))
module = addon.module()
It creates problem - IDEs doesn't know that I injected the name "this".
Let's solve this!
from addon_system import resolve_runtime, Addon
this = resolve_runtime(Addon)
print("Addon module received \"this\" variable with value:", this)
Note:
resolve_runtime
also checks requested type with
a provided value type and will raise TypeError if it is differentresolve_runtime
automatically resolves the name of
required variable, but you can also pass it manually, by parameter name
Module Interface
Wait, what? My IDE now suggests to me methods that I could call!
How this works: you create class-representation of the module -
library instantiates it with addon and allows you to use it anywhere.
Simple example:
from pathlib import Path
from typing import Any
from addon_system import AddonSystem, ModuleInterface
from addon_system.libraries.pip_manager import PipLibManager
class MyInterface(ModuleInterface):
def get_supported_events(self) -> list[str]:
"""Returns supported events by this addon"""
return self.get_func("get_supported_events")()
def propagate_event(self, event_name: str,
event_data: dict[str, Any]) -> bool:
"""Propagates event to this addon, and return True if handled"""
handler = self.get_func("on_" + event_name)
if handler is None:
return False
return handler(event_data)
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
addon.namespace.update(dict(this=addon))
interface = addon.interface(
MyInterface,
"supported on_load positional argument",
kwd="supported on_load keyword argument",
)
if "smth" in interface.get_supported_events():
interface.propagate_event("smth", dict(issued_by="User"))
ModuleInterface class has built-in methods to manipulate on addon's module:
get_func(name: str)
— returns function with the given name, if it is not a function — returns None
get_attr(name: str[, default=Any])
— get attribute by the given name, if default is not set - raises AttributeError
set_attrs(**names)
— set passed keyword arguments as module attributes
Module unloading
Free my memory, please
Library can try to unload addon's modules
Note: It works better in a pair of ModuleInterface
from pathlib import Path
from addon_system import AddonSystem, ModuleInterface
from addon_system.libraries.pip_manager import PipLibManager
root = Path() / "addons"
system = AddonSystem(root, PipLibManager())
addon = system.get_addon_by_id("KuyuGama/SomeAddon")
module = addon.module()
del module
addon.unload_module()
interface = addon.interface(ModuleInterface)
interface.unload("Argument passed to on_unload module method")
NOTE!!! Unload may not work in case if module is used anywhere else.
If you use interface - use it instance instead of the addon's module
Also: to unload used modules by this addon => you need to
return a list of these modules from on_load module method
(it will be called automatically by ModuleInterface
class)
I recommend not using addon's module object, instead use ModuleInterface.
That is a good idea, because it will unload module if necessary
Addon creation tool
It is easy to create addon!
Library provides tools to create addons via terminal and code:
Command-line addon building
make-addon
is designed to create addons easily using terminal
Parameters:
-n / --name — Addon name(must be CamelCase because tool creates addon directory with the same name)
-a / --authors — Comma separated author names
-i / --id — Addon id (If not provided will be created using first author name + "/" + addon name)
-m / --module — Set the main module name of this addon (Useful when creating from source code)
-p / --package — Path to package that will be used as addon source
-v / --version — Version of addon (Usually SemVer)
-d / --description — Description of addon
-D / --depends — Comma separated addon dependencies (in pip freeze
format)
-t / --template — Path to module template file (Will be used if no source package is provided)
-f / --force — Force create addon (rewrites addon if exists)
-b / --bake — Build "baked" addon using pybaked
library. I Recommend
use it in pair with --package parameter
place_to — Directory where addon will be created
In-code addon building
addon_system.addon.builder.AddonBuilder
and addon_system.addon.builder.AddonPackageBuilder
are designed to build addons from code easily
addon_system.addon.builder.AddonBuilder
Methods:
meta(name: str, authors: list[str], version: str, depends: list[str], id: str, description: str)
Sets the metadata of this addonpackage(package: AddonPackageBuilder)
Sets the package of this addonbuild(path: str | Path | AddonSystem, addon_dir_name: str = None)
Builds addon at given path. If the path is AddonSystem object then addon_dir_name must be passed (addon's root)
addon_system.addon.builder.AddonPackageBuilder
Methods:
- [classmethod]
from_path(path: str | Path)
Create AddonPackageBuilder instance from path. Includes all modules and child packages within given path add(module: StringModule | ModuleType | AddonPackageBuilder)
Add module or sub package to this packagebuild(path: str | Path, unpack: bool = False)
Build this package at given path
If unpack
set to True
- will source of this package at root of given path
(Useful if instance is created using from_path
)
Example of building addon from code:
from addon_system.addon.builder import AddonBuilder, AddonPackageBuilder, StringModule
package = AddonPackageBuilder.from_path("addon-source")
package.add(StringModule("print(1, 2, 3)", "__init__"))
package.set_main("__init__")
builder = AddonBuilder()
builder.meta(
name="AddonName",
authors=["KuyuGama"],
version="0.0.1",
depends=["pyyaml==6.0.1"],
id="KuyuGama/AddonName",
description="Addon description"
)
builder.package(package)
builder.build("addons/AddonName")
Addon interface
Independent addon? Huh
Addons are semi-independent components of AddonSystem.
This means you can use addons without AddonSystem(but with some limitations, of course)
Here are the all methods and properties of semi-independent component Addon:
- Properties:
metadata
— Metadata class contains all the metadata of addonpath
— Path to addonupdate_time
— last update time of addon(retrieved from an operating system)module_path
— path to addon's modulenamespace
— custom namespace of all addon's modules (you may need
to edit that to pass desired values on module initialization)module_import_path
— path that passed into importlib.import_module
to import module of addonsystem
— installed AddonSystem for this addon(not for independent usage)enabled
— addon status shortcut(not for independent usage)
- Methods:
-
install_system(system: AddonSystem)
- system — AddonSystem to install
Install AddonSystem to this addon(usually used by AddonSystem)
-
module(lib_manager: BaseLibManager = None, reload: bool = False)
- lib_manager — Library manager, used to check
dependencies before import of module.
You must pass it if you use addon as an independent object
- reload — Pass True if you want to reload module(uses
importlib.reload
)
Import the Addon module
-
interface(cls: type[ModuleInterface], *args, **kwargs)
- cls — subclass of ModuleInterface that will be instantiated
- *args, **kwargs — will be passed to
on_load
method of the module
Create ModuleInterface instance that can be used to access module
variables with IDEs suggestions
-
unload_interface()
- *args, **kwargs — will be passed to
on_unload
method of the addon
Tries to unload module interface
-
unload_module()
Tries to unload module
-
storage()
Get the addon key-value storage.
Use it if your addon has data to store
-
check_dependencies(lib_manager: BaseLibManager = None)
- lib_manager — Library manager, used to check
dependencies before import of module.
You must pass it if you use addon as an independent object
Check addon dependencies
-
satisfy_dependencies(lib_manager: BaseLibManager = None)
- lib_manager — Library manage, used to install libraries.
You must pass it if you use the addon as an independent component
Install dependencies of this addon
-
set_enabled(enabled: bool)
- enabled — status of addon
Get the addon status (not for independent usage)
-
enable()
Enable addon(not for independent usage)
-
disable()
Disable addon(not for independent usage)
-
bake_to_bytes()
"Bake" this addon to bytes using pybaked
library. Returns bytes
-
bake_to_file()
"Bake" this addon to file using pybaked
library.
Returns path to created file
Thanks for using!