Source code for univention.portal
#!/usr/bin/python3
#
# Univention Portal
#
# SPDX-FileCopyrightText: 2020-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
#
import importlib
import os.path
from collections.abc import Iterator
from glob import glob
[docs]
class Plugin(type):
"""Meta class for plugins."""
def __new__(mcs, name, bases, attrs):
new_cls = super().__new__(mcs, name, bases, attrs)
Plugins.add_plugin(new_cls)
return new_cls
[docs]
class Plugins:
"""Register `Plugin` subclasses and iterate over them."""
_plugins: list[Plugin] = []
_imported: dict[str, bool] = {}
def __init__(self, python_path: str) -> None:
"""
:param str python_path: fully dotted Python path that the plugins will
be found below
"""
self.python_path = python_path
self._imported.setdefault(python_path, False)
[docs]
@classmethod
def add_plugin(cls, plugin: Plugin) -> None:
"""
Called by `Plugin` meta class to register a new `Plugin` subclass.
:param type plugin: a `Plugin` subclass
"""
cls._plugins.append(plugin)
def __iter__(self) -> Iterator[Plugin]:
"""
Iterator for registered `Plugin` subclasses.
:return: `Plugin` subclass
:rtype: type
"""
self.load()
for plugin in self._plugins:
if plugin.__module__.startswith(self.python_path):
yield plugin
[docs]
def load(self) -> None:
"""Load plugins."""
if self._imported.get(self.python_path):
return
base_module = importlib.import_module(self.python_path)
assert base_module.__file__
base_module_dir = os.path.dirname(base_module.__file__)
path = os.path.join(base_module_dir, "*.py")
for pymodule in sorted(glob(path)):
pymodule_name = os.path.basename(pymodule)[:-3] # without .py
importlib.import_module(f"{self.python_path}.{pymodule_name}")
self._imported[self.python_path] = True
[docs]
def get_all_dynamic_classes() -> Iterator[Plugin]:
yield from Plugins("univention.portal.extensions")
[docs]
def get_dynamic_classes(klass_name: str) -> Plugin:
for extension in get_all_dynamic_classes():
if klass_name == extension.__name__:
return extension
raise KeyError(klass_name)