FreedomBox/plinth/module_loader.py
Sunil Mohan Adapa 38ece87c6c
*: Utilize newer 3.10 syntax for type hints
Tests:

- mypy does not show any errors.

- Installing ejabberd app works. Privileged actions run fine.

- Unit tests work.

- No additional testing was done as type annotations don't have any effect at
runtime.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2023-09-25 20:03:34 -04:00

154 lines
4.7 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Discover, load and manage FreedomBox applications.
"""
import collections
import importlib
import logging
import pathlib
import re
import django
from plinth import cfg
from plinth.signals import pre_module_loading
logger = logging.getLogger(__name__)
loaded_modules = collections.OrderedDict()
_modules_to_load = None
def include_urls():
"""Include the URLs of the modules into main Django project."""
for module_import_path in get_modules_to_load():
module_name = module_import_path.split('.')[-1]
_include_module_urls(module_import_path, module_name)
def load_modules():
"""
Read names of enabled modules in modules/enabled directory and
import them from modules directory.
"""
pre_module_loading.send_robust(sender="module_loader")
for module_import_path in get_modules_to_load():
module_name = module_import_path.split('.')[-1]
try:
loaded_modules[module_name] = importlib.import_module(
module_import_path)
except Exception as exception:
logger.exception('Could not import %s: %s', module_import_path,
exception)
if cfg.develop:
raise
def _include_module_urls(module_import_path, module_name):
"""Include the module's URLs in global project URLs list"""
from plinth import urls
url_module = module_import_path + '.urls'
try:
urls.urlpatterns += [
django.urls.re_path(r'',
django.urls.include((url_module, module_name)))
]
except ImportError:
logger.debug('No URLs for %s', module_name)
if cfg.develop:
raise
def _get_modules_enabled_paths():
"""Return list of paths from which enabled modules list must be read."""
return [
pathlib.Path('/usr/share/freedombox/modules-enabled/'),
pathlib.Path('/etc/plinth/modules-enabled/'), # For compatibility
pathlib.Path('/etc/freedombox/modules-enabled/'),
]
def _get_modules_enabled_files_to_read():
"""Return the list of files containing modules import paths."""
module_files = {}
for path in _get_modules_enabled_paths():
directory = pathlib.Path(path)
files = list(directory.glob('*'))
for file_ in files:
# Omit hidden or backup files
if file_.name.startswith('.') or '.dpkg' in file_.name:
continue
if file_.is_symlink() and str(file_.resolve()) == '/dev/null':
module_files.pop(file_.name, None)
continue
module_files[file_.name] = file_
if module_files:
return module_files.values()
# './setup.py install' has not been executed yet. Pickup files to load
# from local module directories.
directory = pathlib.Path(__file__).parent
glob_pattern = 'modules/*/data/usr/share/freedombox/modules-enabled/*'
return list(directory.glob(glob_pattern))
def get_modules_to_load():
"""Get the list of modules to be loaded"""
global _modules_to_load
if _modules_to_load is not None:
return _modules_to_load
modules = []
for file_ in _get_modules_enabled_files_to_read():
module = _read_module_import_paths_from_file(file_)
if module:
modules.append(module)
_modules_to_load = modules
return modules
def get_module_import_path(module_name: str) -> str:
"""Return the import path for a module."""
import_path_file = None
for path in _get_modules_enabled_paths():
file_ = path / module_name
if file_.exists():
import_path_file = file_
if file_.is_symlink() and str(file_.resolve()) == '/dev/null':
import_path_file = None
if not import_path_file:
# './setup.py install' has not been executed yet. Pickup files to load
# from local module directories.
directory = pathlib.Path(__file__).parent
import_path_file = (directory /
f'modules/{module_name}/data/usr/share/'
f'freedombox/modules-enabled/{module_name}')
if not import_path_file:
raise ValueError('Unknown module')
import_path = _read_module_import_paths_from_file(import_path_file)
if not import_path:
raise ValueError('Module disabled')
return import_path
def _read_module_import_paths_from_file(file_path: pathlib.Path) -> str | None:
"""Read a module's import path from a file."""
with file_path.open() as file_handle:
for line in file_handle:
line = re.sub('#.*', '', line)
line = line.strip()
if line:
return line
return None