Derive the data folder of a plugin from its settings class

A plugin with a settings class already names itself in `plugin_name`;
passing the name again to plugin_data_folder() repeats it and can drift.
Add a `data_folder()` class method on MVTPluginSettings which returns
plugin_data_folder() for the class's validated plugin name (works on the
class and on an instance); plugin_data_folder(name) stays as the function
underneath for plugins without a settings class.
This commit is contained in:
Donncha Ó Cearbhaill
2026-08-26 12:05:35 +02:00
parent 90fc0ae351
commit ecc22f54b7
3 changed files with 49 additions and 10 deletions
+14 -9
View File
@@ -37,29 +37,34 @@ leaves a partially written settings file behind.
## Plugin Data Folder ## Plugin Data Folder
Everything else a plugin keeps on disk, such as a cache, a downloaded artifact Everything else a plugin keeps on disk, such as a cache, a downloaded artifact
or synchronization state, belongs in the folder returned by or synchronization state, belongs in the folder returned by the `data_folder()`
`mvt.common.plugin_config.plugin_data_folder()`: class method of the plugin's settings class, or by
`mvt.common.plugin_config.plugin_data_folder()` called with the plugin name if
the plugin has no settings class:
``` ```
~/.local/share/mvt/plugin-data/<plugin name>/ # Linux ~/.local/share/mvt/plugin-data/<plugin name>/ # Linux
~/Library/Application Support/mvt/plugin-data/<plugin name>/ # macOS ~/Library/Application Support/mvt/plugin-data/<plugin name>/ # macOS
``` ```
The folder sits beside MVT's own data, such as the downloaded indicators. The folder sits beside MVT's own data, such as the downloaded indicators. It is
`plugin_data_folder()` creates it if it is missing, with `0700` permissions, created if it is missing, with `0700` permissions. Asking for it again returns
and returns its path. Calling it again returns the same path and leaves the the same path and leaves the contents alone, so a plugin can ask for it every
contents alone, so a plugin can call it every time it needs the folder: time it needs the folder. `ExamplePluginSettings` below is the settings class
defined in the next section:
```python ```python
import os import os
from mvt.common.plugin_config import plugin_data_folder
def cache_path() -> str: def cache_path() -> str:
return os.path.join(plugin_data_folder("example-plugin"), "results.json") folder = ExamplePluginSettings.data_folder()
return os.path.join(folder, "virustotal_lookups_cache.json")
``` ```
A plugin which has no settings class calls
`plugin_data_folder("example-plugin")` instead.
Do not fall back on a path of your own such as `~/.cache/example-plugin`: it Do not fall back on a path of your own such as `~/.cache/example-plugin`: it
is a Linux-only convention, and MVT will not create it for you. is a Linux-only convention, and MVT will not create it for you.
+14 -1
View File
@@ -84,7 +84,8 @@ def plugin_data_folder(plugin_name: str) -> str:
Plugins should keep whatever they persist, such as caches or downloaded Plugins should keep whatever they persist, such as caches or downloaded
artifacts, in this folder. It is created with owner-only permissions. The artifacts, in this folder. It is created with owner-only permissions. The
path is resolved on every call so it always reflects the current path is resolved on every call so it always reflects the current
environment. environment. A plugin with a settings class calls
`MVTPluginSettings.data_folder()` instead, which passes `plugin_name` here.
:param plugin_name: Name of the plugin. :param plugin_name: Name of the plugin.
:returns: The path of the data folder of the plugin. :returns: The path of the data folder of the plugin.
@@ -160,6 +161,8 @@ class MVTPluginSettings(BaseSettings):
Plugins must not store their settings in MVT's own configuration file: MVT Plugins must not store their settings in MVT's own configuration file: MVT
rewrites it with the fields it knows about, dropping anything else. rewrites it with the fields it knows about, dropping anything else.
`data_folder()` returns the folder the plugin keeps its data in.
""" """
model_config = SettingsConfigDict(extra="ignore") model_config = SettingsConfigDict(extra="ignore")
@@ -199,6 +202,16 @@ class MVTPluginSettings(BaseSettings):
""" """
return cls() return cls()
@classmethod
def data_folder(cls) -> str:
"""
Return the data folder of the plugin, creating it.
The folder is the one plugin_data_folder() returns for `plugin_name`,
so a plugin with a settings class does not repeat its name.
"""
return plugin_data_folder(_settings_plugin_name(cls))
def _environment_values(self) -> Dict[str, Any]: def _environment_values(self) -> Dict[str, Any]:
""" """
Return the settings values currently supplied by the environment. Return the settings values currently supplied by the environment.
+21
View File
@@ -368,3 +368,24 @@ def test_unsafe_plugin_names_have_no_data_folder(data_folder, plugin_name):
plugin_data_folder(plugin_name) plugin_data_folder(plugin_name)
assert not os.path.exists(data_folder) assert not os.path.exists(data_folder)
def test_settings_class_knows_its_data_folder(data_folder):
folder = ExamplePluginSettings.data_folder()
assert folder == plugin_data_folder("example-plugin")
assert os.path.isdir(folder)
assert OtherPluginSettings.data_folder() != folder
def test_settings_instance_uses_the_same_data_folder(config_folder, data_folder):
settings = ExamplePluginSettings.load()
assert settings.data_folder() == ExamplePluginSettings.data_folder()
def test_subclass_without_its_own_name_shares_the_data_folder(data_folder):
class InheritingSettings(ExamplePluginSettings):
pass
assert InheritingSettings.data_folder() == ExamplePluginSettings.data_folder()