Add the mvt.plugin import surface (#901)

* Add the mvt.plugin import surface

mvt.plugin re-exports the names a plugin needs from MVT under one import
path. It holds the module base classes and Command, the alert and result
types, the database errors a module raises, the timestamp converters, the
plugin settings API, MVT's settings, get_plugin_logger() and MVT_VERSION.

The names it exports are kept working on a best-effort basis. Changes to
them are announced in the release notes. Anything else in mvt can still be
imported, and may change between releases without notice.

get_plugin_logger(__name__) returns a logger under mvt.ext for plugin code
outside a module class. Its records then reach the console and the
command.log file of a run. A file loaded with --load-module or
--load-command is named after the file.

* Document how to write MVT plugins

The custom modules page now leads with plugin packages. Loading module
files with --load-module and MVT_CUSTOM_MODULES moves to a section on
developing a module locally.

A new "Writing a module" section shows a module which subclasses
IOSExtraction. It lists each base class, the command pair it serves and the
helpers it provides. "Depending on a built-in module" says to import a
built-in class from its family package.

"Importing from MVT" says what mvt.plugin exports and what importing from
it means. The custom commands page shows a Command subclass which lists its
own modules. The sysdiagnose and plugin configuration pages import from
mvt.plugin.
This commit is contained in:
Donncha Ó Cearbhaill
2026-08-27 14:47:16 +02:00
committed by GitHub
parent 85adb02eb9
commit 0b5b3f2d7c
10 changed files with 448 additions and 89 deletions
+29 -2
View File
@@ -1,9 +1,14 @@
from pathlib import Path
import pytest
from mvt.common.cli_plugins import _module_name_for_path as _command_module_name
from mvt.common.module import MVTModule
from mvt.common.module_loader import (
CustomModuleLoadError,
_module_name_for_path,
get_module_logger,
get_plugin_logger,
load_custom_modules,
load_custom_modules_from_path,
module_supports_command,
@@ -168,9 +173,9 @@ def test_get_module_logger_strips_the_plugin_package_prefix():
class PluginModule(MVTModule):
pass
PluginModule.__module__ = "mvt_plugin_amnesty_custom.ios.custom"
PluginModule.__module__ = "mvt_plugin_example_org.ios.custom"
assert get_module_logger(PluginModule).name == "mvt.ext.amnesty_custom.ios.custom"
assert get_module_logger(PluginModule).name == "mvt.ext.example_org.ios.custom"
def test_get_module_logger_only_strips_the_prefix_from_the_top_level():
@@ -187,3 +192,25 @@ def test_get_module_logger_names_path_modules_after_their_file(tmp_path):
module = load_custom_modules_from_path(str(module_path))[0]
assert get_module_logger(module).name == "mvt.ext.my_custom_module"
def test_get_plugin_logger_uses_the_same_namespace_as_modules():
assert (
get_plugin_logger("mvt_plugin_example_org.commands.summarize").name
== "mvt.ext.example_org.commands.summarize"
)
assert get_plugin_logger("example_plugin.cli").name == "mvt.ext.example_plugin.cli"
def test_get_plugin_logger_keeps_builtin_names():
assert get_plugin_logger("mvt.ios.cli").name == "mvt.ios.cli"
def test_get_plugin_logger_names_loaded_files_after_the_file():
# A file loaded with --load-command or --load-module is imported under a
# mangled name. The log names the file instead.
command_name = _command_module_name(Path("/tmp/case_summary.py"))
module_name = _module_name_for_path(Path("/tmp/my_custom_module.py"))
assert get_plugin_logger(command_name).name == "mvt.ext.case_summary"
assert get_plugin_logger(module_name).name == "mvt.ext.my_custom_module"
+34
View File
@@ -0,0 +1,34 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2026 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import mvt.plugin
from mvt.android.modules.backup.base import BackupModule
from mvt.common.config import settings
from ..plugin_fixtures import run_isolated_python
def test_the_exported_names_are_the_public_names():
public = {name for name in vars(mvt.plugin) if not name.startswith("_")}
assert public == set(mvt.plugin.__all__)
assert mvt.plugin.settings is settings
assert mvt.plugin.AndroidBackupModule is BackupModule
def test_the_surface_imports_before_anything_else_of_mvt(tmp_path):
# A plugin can import the surface as its first import of MVT. The
# subprocess gets a temporary home because importing MVT writes its
# configuration file.
result = run_isolated_python(
"from mvt.plugin import IOSExtraction, MVT_VERSION, settings\n"
"assert MVT_VERSION\n"
"assert settings.NETWORK_TIMEOUT > 0\n"
"assert IOSExtraction.__name__ == 'IOSExtraction'\n",
home=tmp_path,
)
assert result.returncode == 0, result.stderr
assert result.stderr == ""
+1
View File
@@ -73,6 +73,7 @@ def run_isolated_python(
}
isolated_environment["HOME"] = str(home)
isolated_environment["XDG_CONFIG_HOME"] = str(home / "config")
isolated_environment["XDG_DATA_HOME"] = str(home / "data")
if site_path is not None:
isolated_environment["PYTHONPATH"] = str(site_path)
isolated_environment.update(environment)