diff --git a/docs/development/plugin_configuration.md b/docs/development/plugin_configuration.md index 69a47bd..e41f4e4 100644 --- a/docs/development/plugin_configuration.md +++ b/docs/development/plugin_configuration.md @@ -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// # Linux ~/Library/Application Support/mvt/plugin-data// # 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. diff --git a/src/mvt/common/plugin_config.py b/src/mvt/common/plugin_config.py index 8c50262..f5e2099 100644 --- a/src/mvt/common/plugin_config.py +++ b/src/mvt/common/plugin_config.py @@ -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. diff --git a/tests/common/test_plugin_config.py b/tests/common/test_plugin_config.py index 172dc68..9bcf9eb 100644 --- a/tests/common/test_plugin_config.py +++ b/tests/common/test_plugin_config.py @@ -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()