huge refactor of plugin system
This commit is contained in:
+12
-7
@@ -5,6 +5,7 @@ from importlib import import_module, reload as reload_module
|
||||
from types import ModuleType
|
||||
from typing import Any, Dict, List, Optional
|
||||
import logging
|
||||
from ProviderCore.commands import get_primary_command_object
|
||||
from ProviderCore.registry import get_plugin
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -71,17 +72,22 @@ def _normalize_mod_name(mod_name: str) -> str:
|
||||
|
||||
|
||||
def import_cmd_module(mod_name: str, *, reload_loaded: bool = False):
|
||||
"""Import a cmdlet/native module from cmdnat or cmdlet packages."""
|
||||
"""Import a cmdlet/command module from legacy or plugin-owned packages."""
|
||||
normalized = _normalize_mod_name(mod_name)
|
||||
if not normalized:
|
||||
return None
|
||||
for package in ("cmdnat", "cmdlet", None):
|
||||
for qualified in (
|
||||
f"plugins.{normalized}.commands",
|
||||
f"cmdnat.{normalized}",
|
||||
f"cmdlet.{normalized}",
|
||||
normalized,
|
||||
):
|
||||
try:
|
||||
# When attempting a bare import (package is None), prefer the repo-local
|
||||
# `MPV` package for the `mpv` module name so we don't accidentally
|
||||
# import the third-party `mpv` package (python-mpv) which can raise
|
||||
# OSError if system libmpv is missing.
|
||||
if package is None and normalized == "mpv":
|
||||
if qualified == normalized and normalized == "mpv":
|
||||
try:
|
||||
if reload_loaded and "MPV" in sys.modules:
|
||||
return reload_module(sys.modules["MPV"])
|
||||
@@ -90,7 +96,6 @@ def import_cmd_module(mod_name: str, *, reload_loaded: bool = False):
|
||||
# Local MPV package not present; fall back to the normal bare import.
|
||||
pass
|
||||
|
||||
qualified = f"{package}.{normalized}" if package else normalized
|
||||
if reload_loaded and qualified in sys.modules:
|
||||
return reload_module(sys.modules[qualified])
|
||||
return import_module(qualified)
|
||||
@@ -151,7 +156,7 @@ def get_cmdlet_metadata(
|
||||
ensure_registry_loaded()
|
||||
normalized = cmd_name.replace("-", "_")
|
||||
mod = import_cmd_module(normalized)
|
||||
data = getattr(mod, "CMDLET", None) if mod else None
|
||||
data = get_primary_command_object(mod) if mod else None
|
||||
|
||||
if data is None:
|
||||
try:
|
||||
@@ -161,7 +166,7 @@ def get_cmdlet_metadata(
|
||||
owner_mod = getattr(reg_fn, "__module__", "")
|
||||
if owner_mod:
|
||||
owner = import_module(owner_mod)
|
||||
data = getattr(owner, "CMDLET", None)
|
||||
data = get_primary_command_object(owner)
|
||||
except Exception as exc:
|
||||
logger.exception("Registry fallback failed while resolving cmdlet %s: %s", cmd_name, exc)
|
||||
data = None
|
||||
@@ -371,7 +376,7 @@ def get_cmdlet_arg_choices(
|
||||
ids = []
|
||||
|
||||
if ids:
|
||||
# Try to resolve names via Provider.matrix if config provides auth info
|
||||
# Try to resolve names via the Matrix plugin if config provides auth info
|
||||
try:
|
||||
hs = matrix_conf.get("homeserver")
|
||||
token = matrix_conf.get("access_token")
|
||||
|
||||
+7
-3
@@ -33,13 +33,17 @@ class CmdletArg:
|
||||
return value
|
||||
|
||||
def to_flags(self) -> tuple[str, ...]:
|
||||
flags = [f"--{self.name}", f"-{self.name}"]
|
||||
normalized_name = str(self.name or "").lstrip("-")
|
||||
if not normalized_name:
|
||||
return tuple()
|
||||
|
||||
flags = [f"--{normalized_name}", f"-{normalized_name}"]
|
||||
if self.alias:
|
||||
flags.append(f"-{self.alias}")
|
||||
|
||||
if self.type == "flag":
|
||||
flags.append(f"--no-{self.name}")
|
||||
flags.append(f"-no{self.name}")
|
||||
flags.append(f"--no-{normalized_name}")
|
||||
flags.append(f"-no{normalized_name}")
|
||||
if self.alias:
|
||||
flags.append(f"-n{self.alias}")
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable, List, Optional, Sequence
|
||||
|
||||
VALUE_ARG_FLAGS = frozenset({"-value", "--value", "-set-value", "--set-value"})
|
||||
|
||||
|
||||
def extract_piped_value(result: Any) -> Optional[str]:
|
||||
if isinstance(result, str):
|
||||
return result.strip() if result.strip() else None
|
||||
if isinstance(result, (int, float)):
|
||||
return str(result)
|
||||
if isinstance(result, dict):
|
||||
value = result.get("value")
|
||||
if value is not None:
|
||||
return str(value).strip()
|
||||
return None
|
||||
|
||||
|
||||
def extract_arg_value(
|
||||
args: Sequence[str],
|
||||
*,
|
||||
flags: Iterable[str],
|
||||
allow_positional: bool = False,
|
||||
) -> Optional[str]:
|
||||
if not args:
|
||||
return None
|
||||
|
||||
tokens = [str(tok) for tok in args if tok is not None]
|
||||
normalized_flags = {
|
||||
str(flag).strip().lower() for flag in flags if str(flag).strip()
|
||||
}
|
||||
if not normalized_flags:
|
||||
return None
|
||||
|
||||
for idx, tok in enumerate(tokens):
|
||||
text = tok.strip()
|
||||
if not text:
|
||||
continue
|
||||
low = text.lower()
|
||||
if low in normalized_flags and idx + 1 < len(tokens):
|
||||
candidate = str(tokens[idx + 1]).strip()
|
||||
if candidate:
|
||||
return candidate
|
||||
if "=" in low:
|
||||
head, value = low.split("=", 1)
|
||||
if head in normalized_flags and value:
|
||||
return value.strip()
|
||||
|
||||
if not allow_positional:
|
||||
return None
|
||||
|
||||
for tok in tokens:
|
||||
text = str(tok).strip()
|
||||
if text and not text.startswith("-"):
|
||||
return text
|
||||
return None
|
||||
|
||||
|
||||
def extract_value_arg(args: Sequence[str]) -> Optional[str]:
|
||||
return extract_arg_value(args, flags=VALUE_ARG_FLAGS, allow_positional=True)
|
||||
|
||||
|
||||
def has_flag(args: Sequence[str], flag: str) -> bool:
|
||||
try:
|
||||
want = str(flag or "").strip().lower()
|
||||
if not want:
|
||||
return False
|
||||
return any(str(arg).strip().lower() == want for arg in (args or []))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def normalize_to_list(value: Any) -> List[Any]:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return [value]
|
||||
@@ -769,14 +769,6 @@ def set_last_result_table_overlay(
|
||||
state.display_items = items or []
|
||||
state.display_subject = subject
|
||||
|
||||
def set_last_result_table_preserve_history(
|
||||
result_table: Optional[Any],
|
||||
items: Optional[List[Any]] = None,
|
||||
subject: Optional[Any] = None
|
||||
) -> None:
|
||||
"""Compatibility alias for set_last_result_table_overlay."""
|
||||
set_last_result_table_overlay(result_table, items=items, subject=subject)
|
||||
|
||||
def set_last_result_items_only(items: Optional[List[Any]]) -> None:
|
||||
"""
|
||||
Store items for @N selection WITHOUT affecting history or saved search data.
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any, Dict, Iterable, List, Optional
|
||||
|
||||
from SYS.config import global_config
|
||||
from ProviderCore.registry import get_plugin_class, list_plugins
|
||||
from Store.registry import _discover_store_classes, _required_keys_for
|
||||
from Store.registry import _discover_store_classes, _required_keys_for, _resolve_store_class
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,8 +54,7 @@ def _call_schema(owner: Any, label: str) -> List[ConfigField]:
|
||||
|
||||
|
||||
def get_store_schema(store_type: str) -> List[ConfigField]:
|
||||
classes = _discover_store_classes()
|
||||
cls = classes.get(str(store_type or "").strip())
|
||||
cls = _resolve_store_class(str(store_type or "").strip())
|
||||
if cls is None:
|
||||
return []
|
||||
return _call_schema(cls, f"store '{store_type}'")
|
||||
@@ -115,8 +114,7 @@ def build_default_store_config(store_type: str, instance_name: str) -> Dict[str,
|
||||
config[key] = field.get("default", "")
|
||||
return config
|
||||
|
||||
classes = _discover_store_classes()
|
||||
cls = classes.get(str(store_type or "").strip())
|
||||
cls = _resolve_store_class(str(store_type or "").strip())
|
||||
if cls is None:
|
||||
return config
|
||||
for required_key in _required_keys_for(cls):
|
||||
@@ -174,8 +172,7 @@ def get_required_config_keys(item_type: str, item_name: str) -> List[str]:
|
||||
|
||||
if normalized_type.startswith("store-"):
|
||||
store_type = normalized_type.replace("store-", "", 1)
|
||||
classes = _discover_store_classes()
|
||||
cls = classes.get(store_type)
|
||||
cls = _resolve_store_class(store_type)
|
||||
if cls is not None:
|
||||
for required_key in _required_keys_for(cls):
|
||||
_add_key(required_key)
|
||||
|
||||
+72
-1
@@ -13,7 +13,7 @@ Features:
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Callable, Set
|
||||
from typing import Any, Dict, List, Optional, Callable, Set, Tuple
|
||||
from pathlib import Path
|
||||
import json
|
||||
import re
|
||||
@@ -94,6 +94,42 @@ def _partition_detail_tags(tags: Any) -> tuple[List[str], List[str]]:
|
||||
return namespace_tags, freeform_tags
|
||||
|
||||
|
||||
def _extract_namespace_sort_values(tags: Any, namespace: str) -> List[str]:
|
||||
wanted = str(namespace or "").strip().casefold()
|
||||
if not wanted:
|
||||
return []
|
||||
|
||||
values: List[str] = []
|
||||
for tag in _normalize_detail_tags(tags):
|
||||
ns, sep, value = str(tag).partition(":")
|
||||
if not sep:
|
||||
continue
|
||||
if ns.strip().casefold() != wanted:
|
||||
continue
|
||||
clean_value = value.strip()
|
||||
if clean_value:
|
||||
values.append(clean_value)
|
||||
return values
|
||||
|
||||
|
||||
def _namespace_sort_key(tags: Any, namespace: str, *, numeric: bool = False) -> Tuple[int, Any, str]:
|
||||
values = _extract_namespace_sort_values(tags, namespace)
|
||||
if not values:
|
||||
return (1, float("inf") if numeric else "", "")
|
||||
|
||||
primary = values[0]
|
||||
if numeric:
|
||||
match = re.search(r"-?\d+(?:\.\d+)?", primary)
|
||||
if match:
|
||||
try:
|
||||
return (0, float(match.group(0)), primary.casefold())
|
||||
except Exception:
|
||||
pass
|
||||
return (0, float("inf"), primary.casefold())
|
||||
|
||||
return (0, primary.casefold(), primary.casefold())
|
||||
|
||||
|
||||
def _chunk_detail_tags(tags: List[str], columns: int) -> List[List[str]]:
|
||||
column_count = max(1, int(columns or 1))
|
||||
rows: List[List[str]] = []
|
||||
@@ -954,6 +990,41 @@ class Table:
|
||||
|
||||
return self
|
||||
|
||||
def sort_by_tag_namespace(self, namespace: str, *, numeric: bool = False, reverse: bool = False) -> "Table":
|
||||
"""Sort rows by the first value found for a tag namespace.
|
||||
|
||||
Looks first at row payload tag metadata, then falls back to the visible Tag column.
|
||||
When ``numeric`` is True, the first numeric token inside the namespace value is used.
|
||||
"""
|
||||
if getattr(self, "preserve_order", False):
|
||||
return self
|
||||
|
||||
wanted = str(namespace or "").strip()
|
||||
if not wanted:
|
||||
return self
|
||||
|
||||
def _row_tags(row: Row) -> Any:
|
||||
payload = getattr(row, "payload", None)
|
||||
if isinstance(payload, dict):
|
||||
for key in ("tag", "tags", "tag_summary"):
|
||||
value = payload.get(key)
|
||||
if value:
|
||||
return value
|
||||
metadata = payload.get("metadata")
|
||||
if isinstance(metadata, dict):
|
||||
for key in ("tag", "tags", "tag_summary"):
|
||||
value = metadata.get(key)
|
||||
if value:
|
||||
return value
|
||||
tag_column = row.get_column("Tag")
|
||||
return tag_column or []
|
||||
|
||||
self.rows.sort(
|
||||
key=lambda row: _namespace_sort_key(_row_tags(row), wanted, numeric=bool(numeric)),
|
||||
reverse=bool(reverse),
|
||||
)
|
||||
return self
|
||||
|
||||
def add_result(self, result: Any) -> "Table":
|
||||
"""Add a result object (SearchResult, PipeObject, ResultItem, TagItem, or dict) as a row.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user