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
Everything else a plugin keeps on disk, such as a cache, a downloaded artifact
or synchronization state, belongs in the folder returned by
`mvt.common.plugin_config.plugin_data_folder()`:
or synchronization state, belongs in the folder returned by the `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
~/Library/Application Support/mvt/plugin-data/<plugin name>/ # macOS
```
The folder sits beside MVT's own data, such as the downloaded indicators.
`plugin_data_folder()` creates it if it is missing, with `0700` permissions,
and returns its path. Calling it again returns the same path and leaves the
contents alone, so a plugin can call it every time it needs the folder:
The folder sits beside MVT's own data, such as the downloaded indicators. It is
created if it is missing, with `0700` permissions. Asking for it again returns
the same path and leaves the contents alone, so a plugin can ask for it every
time it needs the folder. `ExamplePluginSettings` below is the settings class
defined in the next section:
```python
import os
from mvt.common.plugin_config import plugin_data_folder
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
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
artifacts, in this folder. It is created with owner-only permissions. The
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.
: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
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")
@@ -199,6 +202,16 @@ class MVTPluginSettings(BaseSettings):
"""
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]:
"""
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)
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()