huge refactor of the entire codebase, with the goal of improving maintainability, readability, and extensibility. This commit includes changes to almost every file in the project, including:

This commit is contained in:
2026-04-19 00:41:09 -07:00
parent d9e736172a
commit bafd37fdfb
50 changed files with 3258 additions and 4177 deletions
+4 -3
View File
@@ -1,5 +1,6 @@
"""Provider core modules.
"""Plugin core modules.
This package contains the provider framework (base types, registry, and shared helpers).
Concrete provider implementations live in the `Provider/` package.
This package contains the plugin framework (base types, registry, and shared
helpers). Built-in plugins continue to live in the `Provider/` package for
backward compatibility.
"""
+139 -16
View File
@@ -10,9 +10,9 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Callable
@dataclass
class SearchResult:
"""Unified search result format across all search providers."""
"""Unified search result format across all search plugins."""
table: str # Provider name: "libgen", "soulseek", "bandcamp", "youtube", etc.
table: str # Plugin name: "libgen", "soulseek", "bandcamp", "youtube", etc.
title: str # Display title/filename
path: str # Download target (URL, path, magnet, identifier)
@@ -84,7 +84,7 @@ class SearchResult:
def parse_inline_query_arguments(raw_query: str) -> Tuple[str, Dict[str, str]]:
"""Extract inline key:value arguments from a provider search query."""
"""Extract inline key:value arguments from a plugin search query."""
query_text = str(raw_query or "").strip()
if not query_text:
@@ -112,10 +112,10 @@ def parse_inline_query_arguments(raw_query: str) -> Tuple[str, Dict[str, str]]:
class Provider(ABC):
"""Unified provider base class.
"""Unified plugin base class.
This replaces the older split between "search providers" and "file providers".
Concrete providers may implement any subset of:
This replaces the older split between search and upload providers.
Concrete plugins may implement any subset of:
- search(query, ...)
- download(result, output_dir)
- upload(file_path, ...)
@@ -124,7 +124,8 @@ class Provider(ABC):
"""
URL: Sequence[str] = ()
NAME: str = ""
PLUGIN_NAME: str = ""
PLUGIN_ALIASES: Sequence[str] = ()
# Optional provider-driven defaults for what to do when a user selects @N from a
# provider table. The CLI uses this to auto-insert stages (e.g. download-file)
@@ -141,24 +142,23 @@ class Provider(ABC):
# Used for dynamically generating config panels (e.g., missing credentials).
REQUIRED_CONFIG_KEYS: Sequence[str] = ()
# Some providers implement `upload()` but are not intended to be used as
# generic "file host" providers via `add-file -provider ...`.
# Some plugins implement `upload()` but are not intended to be used as
# generic "file host" plugins via `add-file -plugin ...`.
EXPOSE_AS_FILE_PROVIDER: bool = True
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = config or {}
# Prioritize explicit NAME property for the instance name
self.name = str(
getattr(self, "NAME", None)
or getattr(self, "PROVIDER_NAME", None)
getattr(self, "PLUGIN_NAME", None)
or self.__class__.__name__
).lower()
@property
def label(self) -> str:
"""Friendly display name for the provider."""
if hasattr(self, "NAME") and self.NAME:
name = str(self.NAME)
"""Friendly display name for the plugin."""
name = str(getattr(self, "PLUGIN_NAME", None) or self.__class__.__name__)
if name:
if name.lower() == "loc":
return "LoC"
if name.lower() == "openlibrary":
@@ -186,7 +186,7 @@ class Provider(ABC):
def get_table_metadata(self, query: str, filters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Return metadata for the results table."""
return {"provider": self.name}
return {"plugin": self.name}
def get_source_command(self, args_list: List[str]) -> Tuple[str, List[str]]:
"""Return the command and arguments that produced this search result.
@@ -308,6 +308,49 @@ class Provider(ABC):
_ = config
return 0
def resolve_pipe_result_download(
self,
result: Any,
pipe_obj: Any,
) -> Tuple[Optional[Path], Optional[str], Optional[Path]]:
"""Materialize a piped plugin result into a local file for add-file."""
_ = result
_ = pipe_obj
return None, None, None
def expand_selection(
self,
selected_items: List[Any],
*,
ctx: Any,
stage_is_last: bool = True,
table_type: str = "",
**_kwargs: Any,
) -> Optional[List[Any]]:
"""Optionally expand a selection into downstream items for non-terminal pipelines."""
_ = selected_items
_ = ctx
_ = stage_is_last
_ = table_type
return None
def status_summary(self) -> Dict[str, Any]:
"""Return plugin-owned status details for startup/status views."""
enabled = False
try:
enabled = bool(self.validate())
except Exception:
enabled = False
return {
"status": "ENABLED" if enabled else "DISABLED",
"name": self.label,
"plugin": self.name,
"detail": "Configured" if enabled else "Not configured",
}
def handle_url(self, url: str, *, output_dir: Optional[Path] = None) -> Tuple[bool, Optional[Path | Dict[str, Any]]]:
"""Optional provider override to parse and act on URLs."""
@@ -315,6 +358,67 @@ class Provider(ABC):
_ = output_dir
return False, None
def download_url(self, url: str, output_dir: Path, **_kwargs: Any) -> Optional[Any]:
"""Optional direct-URL download hook used by generic cmdlets."""
_ = url
_ = output_dir
return None
def resolve_url(self, url: str, **_kwargs: Any) -> str:
"""Optionally normalize or exchange a URL before downstream use."""
return str(url or "")
def resolve_playback_path(self, item: Any, **_kwargs: Any) -> Optional[str]:
"""Optionally turn a plugin-owned item into a playable local path or URL."""
_ = item
return None
def list_url_formats(self, url: str, **_kwargs: Any) -> Optional[List[Dict[str, Any]]]:
"""Optionally return picker-friendly format metadata for a URL."""
_ = url
return None
def filter_picker_formats(
self,
formats: List[Dict[str, Any]],
**_kwargs: Any,
) -> List[Dict[str, Any]]:
"""Optionally filter or reorder raw format rows before UI display."""
return list(formats or [])
def enrich_playlist_entries(
self,
entries: List[Dict[str, Any]],
**_kwargs: Any,
) -> Optional[List[Dict[str, Any]]]:
"""Optionally expand lightweight playlist entries with richer metadata."""
_ = entries
return None
def maybe_show_picker(
self,
*,
url: str,
item: Optional[Any] = None,
parsed: Optional[Dict[str, Any]] = None,
config: Optional[Dict[str, Any]] = None,
quiet_mode: bool = False,
) -> Optional[int]:
"""Optional hook for plugins that want to render an interactive picker/table."""
_ = url
_ = item
_ = parsed
_ = config
_ = quiet_mode
return None
def upload(self, file_path: str, **kwargs: Any) -> str:
"""Upload a file and return a URL or identifier."""
raise NotImplementedError(f"Provider '{self.name}' does not support upload")
@@ -419,6 +523,25 @@ class Provider(ABC):
patterns.append(candidate)
return tuple(patterns)
@classmethod
def selection_url_prefixes(cls) -> Tuple[str, ...]:
"""Return URL-like prefixes that selection parsing should treat as URLs."""
prefixes: List[str] = []
seen: set[str] = set()
for pattern in cls.url_patterns():
try:
candidate = str(pattern or "").strip().lower()
except Exception:
continue
if not candidate:
continue
if "://" in candidate or candidate.endswith(":") or "🧲" in candidate:
if candidate not in seen:
seen.add(candidate)
prefixes.append(candidate)
return tuple(prefixes)
class SearchProvider(Provider):
"""Compatibility alias for older code.
+225 -92
View File
@@ -1,17 +1,21 @@
"""Provider registry.
"""Plugin registry.
Concrete provider implementations live in the ``Provider`` package. This module
is the single source of truth for discovery, metadata, and lifecycle helpers
for those plugins.
Built-in plugin implementations live in the ``Provider`` package. External user
plugins can be dropped into a repo-local ``plugins/`` directory or discovered
via environment-configured plugin paths.
"""
from __future__ import annotations
from functools import lru_cache
import hashlib
import importlib
import importlib.util
import os
import pkgutil
import sys
from dataclasses import dataclass, field
from pathlib import Path
from types import ModuleType
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type
from urllib.parse import urlparse
@@ -21,21 +25,85 @@ from SYS.logger import log, debug
from ProviderCore.base import FileProvider, Provider, SearchProvider, SearchResult
def download_soulseek_file(*args: Any, **kwargs: Any) -> Any:
"""Lazy proxy for the soulseek downloader.
_EXTERNAL_PLUGIN_ENV_VARS: tuple[str, ...] = ("MM_PLUGIN_PATH", "MEDEIA_PLUGIN_PATH")
Importing the provider modules can be expensive; keeping this lazy avoids
paying that cost at registry import time.
"""
from Provider.soulseek import download_soulseek_file as _download
def _repo_root() -> Path:
try:
return Path(__file__).resolve().parents[1]
except Exception:
return Path.cwd()
return _download(*args, **kwargs)
def _iter_external_plugin_dirs() -> Tuple[Path, ...]:
seen: set[str] = set()
dirs: List[Path] = []
candidates: List[Path] = [_repo_root() / "plugins"]
try:
cwd_plugins = Path.cwd() / "plugins"
if cwd_plugins not in candidates:
candidates.append(cwd_plugins)
except Exception:
pass
for env_name in _EXTERNAL_PLUGIN_ENV_VARS:
raw_value = str(os.environ.get(env_name, "") or "").strip()
if not raw_value:
continue
for chunk in raw_value.split(os.pathsep):
text = str(chunk or "").strip().strip('"')
if not text:
continue
candidates.append(Path(text).expanduser())
for candidate in candidates:
try:
resolved = candidate.resolve()
except Exception:
resolved = candidate
key = str(resolved).lower()
if key in seen:
continue
seen.add(key)
try:
if resolved.exists() and resolved.is_dir():
dirs.append(resolved)
except Exception:
continue
return tuple(dirs)
def _iter_external_plugin_entries(plugin_dir: Path) -> Iterable[Tuple[str, Path, bool]]:
try:
children = sorted(plugin_dir.iterdir(), key=lambda entry: entry.name.lower())
except Exception:
return ()
out: List[Tuple[str, Path, bool]] = []
for child in children:
name = str(child.name or "").strip()
if not name or name.startswith("."):
continue
if child.is_file() and child.suffix.lower() == ".py" and child.stem != "__init__":
fingerprint = hashlib.sha1(str(child).encode("utf-8", errors="ignore")).hexdigest()[:10]
out.append((f"_medeia_plugin_{child.stem}_{fingerprint}", child, False))
continue
if child.is_dir():
init_py = child / "__init__.py"
if not init_py.exists() or not init_py.is_file():
continue
fingerprint = hashlib.sha1(str(child).encode("utf-8", errors="ignore")).hexdigest()[:10]
out.append((f"_medeia_plugin_pkg_{child.name}_{fingerprint}", init_py, True))
return tuple(out)
@dataclass(frozen=True)
class ProviderInfo:
"""Metadata about a single provider entry."""
"""Metadata about a single plugin entry."""
canonical_name: str
provider_class: Type[Provider]
@@ -56,14 +124,16 @@ class ProviderInfo:
class ProviderRegistry:
"""Handles discovery, registration, and lookup of provider classes."""
"""Handles discovery, registration, and lookup of built-in and external plugins."""
def __init__(self, package_name: str) -> None:
self.package_name = (package_name or "").strip()
self._infos: Dict[str, ProviderInfo] = {}
self._lookup: Dict[str, ProviderInfo] = {}
self._modules: set[str] = set()
self._external_modules: set[str] = set()
self._discovered = False
self._external_dirs_scanned = False
def _normalize(self, value: Any) -> str:
return str(value or "").strip().lower()
@@ -85,12 +155,10 @@ class ProviderRegistry:
if override_name:
_add(override_name)
else:
# Use explicit NAME or PROVIDER_NAME if available, else class name
_add(getattr(provider_class, "NAME", None))
_add(getattr(provider_class, "PROVIDER_NAME", None))
_add(getattr(provider_class, "PLUGIN_NAME", None))
_add(getattr(provider_class, "__name__", None))
for alias in getattr(provider_class, "PROVIDER_ALIASES", ()) or ():
for alias in getattr(provider_class, "PLUGIN_ALIASES", ()) or ():
_add(alias)
return names
@@ -104,14 +172,14 @@ class ProviderRegistry:
module_name: Optional[str] = None,
replace: bool = False,
) -> ProviderInfo:
"""Register a provider class with canonical and alias names."""
"""Register a plugin class with canonical and alias names."""
candidates = self._candidate_names(provider_class, override_name)
if not candidates:
raise ValueError("provider name candidates are required")
raise ValueError("plugin name candidates are required")
canonical = self._normalize(candidates[0])
if not canonical:
raise ValueError("provider name must not be empty")
raise ValueError("plugin name must not be empty")
alias_names: List[str] = []
alias_seen: set[str] = set()
@@ -165,7 +233,44 @@ class ProviderRegistry:
try:
self.register(candidate, module_name=module_name)
except Exception as exc:
log(f"[provider] Failed to register {module_name}.{candidate.__name__}: {exc}", file=sys.stderr)
log(f"[plugin] Failed to register {module_name}.{candidate.__name__}: {exc}", file=sys.stderr)
def _discover_external_plugins(self) -> None:
if self._external_dirs_scanned:
return
self._external_dirs_scanned = True
for plugin_dir in _iter_external_plugin_dirs():
try:
plugin_dir_str = str(plugin_dir)
if plugin_dir_str and plugin_dir_str not in sys.path:
sys.path.insert(0, plugin_dir_str)
except Exception:
pass
for module_name, module_path, is_package in _iter_external_plugin_entries(plugin_dir):
if module_name in self._external_modules:
continue
try:
if is_package:
spec = importlib.util.spec_from_file_location(
module_name,
str(module_path),
submodule_search_locations=[str(module_path.parent)],
)
else:
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
if spec is None or spec.loader is None:
raise ImportError("missing module spec loader")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
self._external_modules.add(module_name)
self._register_module(module)
except Exception as exc:
log(f"[plugin] Failed to load external plugin {module_path}: {exc}", file=sys.stderr)
def discover(self) -> None:
"""Import and register providers from the package."""
@@ -177,12 +282,13 @@ class ProviderRegistry:
try:
package = importlib.import_module(self.package_name)
except Exception as exc:
log(f"[provider] Failed to import package {self.package_name}: {exc}", file=sys.stderr)
log(f"[plugin] Failed to import package {self.package_name}: {exc}", file=sys.stderr)
return
self._register_module(package)
package_path = getattr(package, "__path__", None)
if not package_path:
self._discover_external_plugins()
return
for finder, module_name, _ in pkgutil.iter_modules(package_path):
@@ -194,18 +300,19 @@ class ProviderRegistry:
try:
module = importlib.import_module(module_path)
except Exception as exc:
log(f"[provider] Failed to load {module_path}: {exc}", file=sys.stderr)
log(f"[plugin] Failed to load {module_path}: {exc}", file=sys.stderr)
continue
self._register_module(module)
# Pick up any Provider subclasses loaded via other mechanisms.
self._sync_subclasses()
self._discover_external_plugins()
def _try_import_for_name(self, normalized_name: str) -> None:
"""Best-effort import for a single provider module.
"""Best-effort import for a single plugin module.
This avoids importing every provider module when the caller only needs
one provider (common for CLI usage).
one plugin (common for CLI usage).
"""
name = str(normalized_name or "").strip().lower()
if not name or not self.package_name:
@@ -249,6 +356,7 @@ class ProviderRegistry:
# module that matches the requested name.
if not self._discovered:
self._try_import_for_name(normalized)
self._discover_external_plugins()
info = self._lookup.get(normalized)
if info is not None:
return info
@@ -279,6 +387,9 @@ class ProviderRegistry:
_walk(Provider)
REGISTRY = ProviderRegistry("Provider")
PLUGIN_REGISTRY = REGISTRY
PluginInfo = ProviderInfo
PluginRegistry = ProviderRegistry
@lru_cache(maxsize=512)
@@ -289,18 +400,16 @@ def _provider_url_patterns(provider_class: Type[Provider]) -> Sequence[str]:
return []
def register_provider(
provider_class: Type[Provider],
def register_plugin(
plugin_class: Type[Provider],
*,
name: Optional[str] = None,
aliases: Optional[Sequence[str]] = None,
module_name: Optional[str] = None,
replace: bool = False,
) -> ProviderInfo:
"""Register a provider class from tests or third-party packages."""
return REGISTRY.register(
provider_class,
plugin_class,
override_name=name,
extra_aliases=aliases,
module_name=module_name,
@@ -308,7 +417,7 @@ def register_provider(
)
def get_provider_class(name: str) -> Optional[Type[Provider]]:
def get_plugin_class(name: str) -> Optional[Type[Provider]]:
info = REGISTRY.get(name)
if info is None:
return None
@@ -323,18 +432,18 @@ def selection_auto_stage_for_table(
if not t:
return None
provider_key = t.split(".", 1)[0] if "." in t else t
provider_class = get_provider_class(provider_key) or get_provider_class(t)
if provider_class is None:
plugin_key = t.split(".", 1)[0] if "." in t else t
plugin_class = get_plugin_class(plugin_key) or get_plugin_class(t)
if plugin_class is None:
return None
try:
return provider_class.selection_auto_stage(t, stage_args)
return plugin_class.selection_auto_stage(t, stage_args)
except Exception:
return None
def is_known_provider_name(name: str) -> bool:
def is_known_plugin_name(name: str) -> bool:
return REGISTRY.has_name(name)
@@ -406,83 +515,83 @@ def _collect_inline_choice_mapping(provider: Provider) -> Dict[str, List[Dict[st
return mapping
def get_provider(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
def get_plugin(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
info = REGISTRY.get(name)
if info is None:
debug(f"[provider] Unknown provider: {name}")
debug(f"[plugin] Unknown plugin: {name}")
return None
try:
provider = info.provider_class(config)
if not provider.validate():
debug(f"[provider] Provider '{name}' is not available")
plugin = info.provider_class(config)
if not plugin.validate():
debug(f"[plugin] Plugin '{name}' is not available")
return None
return provider
return plugin
except Exception as exc:
debug(f"[provider] Error initializing '{name}': {exc}")
debug(f"[plugin] Error initializing '{name}': {exc}")
return None
def list_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
def list_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
availability: Dict[str, bool] = {}
for info in REGISTRY.iter_providers():
try:
provider = info.provider_class(config)
availability[info.canonical_name] = provider.validate()
plugin = info.provider_class(config)
availability[info.canonical_name] = plugin.validate()
except Exception:
availability[info.canonical_name] = False
return availability
def get_search_provider(name: str,
config: Optional[Dict[str, Any]] = None) -> Optional[SearchProvider]:
provider = get_provider(name, config)
if provider is None:
def get_search_plugin(name: str,
config: Optional[Dict[str, Any]] = None) -> Optional[SearchProvider]:
plugin = get_plugin(name, config)
if plugin is None:
return None
if not _supports_search(provider):
debug(f"[provider] Provider '{name}' does not support search")
if not _supports_search(plugin):
debug(f"[plugin] Plugin '{name}' does not support search")
return None
return provider # type: ignore[return-value]
return plugin # type: ignore[return-value]
def list_search_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
def list_search_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
availability: Dict[str, bool] = {}
for info in REGISTRY.iter_providers():
try:
provider = info.provider_class(config)
plugin = info.provider_class(config)
availability[info.canonical_name] = bool(
provider.validate() and info.supports_search
plugin.validate() and info.supports_search
)
except Exception:
availability[info.canonical_name] = False
return availability
def get_file_provider(name: str,
def get_upload_plugin(name: str,
config: Optional[Dict[str, Any]] = None) -> Optional[FileProvider]:
provider = get_provider(name, config)
if provider is None:
plugin = get_plugin(name, config)
if plugin is None:
return None
if not _supports_upload(provider):
debug(f"[provider] Provider '{name}' does not support upload")
if not _supports_upload(plugin):
debug(f"[plugin] Plugin '{name}' does not support upload")
return None
return provider # type: ignore[return-value]
return plugin # type: ignore[return-value]
def list_file_providers(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
def list_upload_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
availability: Dict[str, bool] = {}
for info in REGISTRY.iter_providers():
try:
provider = info.provider_class(config)
plugin = info.provider_class(config)
availability[info.canonical_name] = bool(
provider.validate() and info.supports_upload
plugin.validate() and info.supports_upload
)
except Exception:
availability[info.canonical_name] = False
return availability
def match_provider_name_for_url(url: str) -> Optional[str]:
def match_plugin_name_for_url(url: str) -> Optional[str]:
raw_url = str(url or "").strip()
raw_url_lower = raw_url.lower()
try:
@@ -540,31 +649,31 @@ def match_provider_name_for_url(url: str) -> Optional[str]:
return None
def provider_inline_query_choices(
provider_name: str,
def plugin_inline_query_choices(
plugin_name: str,
field_name: str,
config: Optional[Dict[str, Any]] = None,
) -> List[str]:
"""Return provider-declared inline query choices for a field (e.g., system:GBA).
"""Return plugin-declared inline query choices for a field (e.g., system:GBA).
Providers can expose a mapping via ``QUERY_ARG_CHOICES`` (preferred) or
Plugins can expose a mapping via ``QUERY_ARG_CHOICES`` (preferred) or
``INLINE_QUERY_FIELD_CHOICES`` / ``inline_query_field_choices()``. The helper
keeps completion logic simple and reusable.
"""
pname = str(provider_name or "").strip().lower()
pname = str(plugin_name or "").strip().lower()
field = str(field_name or "").strip().lower()
if not pname or not field:
return []
provider = get_search_provider(pname, config)
if provider is None:
provider = get_provider(pname, config)
if provider is None:
plugin = get_search_plugin(pname, config)
if plugin is None:
plugin = get_plugin(pname, config)
if plugin is None:
return []
try:
mapping = _collect_inline_choice_mapping(provider)
mapping = _collect_inline_choice_mapping(plugin)
if not mapping:
return []
@@ -593,12 +702,32 @@ def provider_inline_query_choices(
return []
def get_provider_for_url(url: str,
config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
name = match_provider_name_for_url(url)
def get_plugin_for_url(url: str,
config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
name = match_plugin_name_for_url(url)
if not name:
return None
return get_provider(name, config)
return get_plugin(name, config)
def list_selection_url_prefixes() -> List[str]:
prefixes: List[str] = []
seen: set[str] = set()
for info in REGISTRY.iter_providers():
try:
values = info.provider_class.selection_url_prefixes()
except Exception:
values = ()
for value in values or ():
try:
normalized = str(value or "").strip().lower()
except Exception:
continue
if not normalized or normalized in seen:
continue
seen.add(normalized)
prefixes.append(normalized)
return prefixes
def resolve_inline_filters(
@@ -657,21 +786,25 @@ def resolve_inline_filters(
__all__ = [
"ProviderInfo",
"PluginInfo",
"Provider",
"SearchProvider",
"FileProvider",
"SearchResult",
"register_provider",
"get_provider",
"list_providers",
"get_search_provider",
"list_search_providers",
"get_file_provider",
"list_file_providers",
"match_provider_name_for_url",
"get_provider_for_url",
"get_provider_class",
"PluginRegistry",
"PLUGIN_REGISTRY",
"register_plugin",
"get_plugin",
"list_plugins",
"get_search_plugin",
"list_search_plugins",
"get_upload_plugin",
"list_upload_plugins",
"match_plugin_name_for_url",
"get_plugin_for_url",
"list_selection_url_prefixes",
"get_plugin_class",
"selection_auto_stage_for_table",
"download_soulseek_file",
"provider_inline_query_choices",
"plugin_inline_query_choices",
"is_known_plugin_name",
]