mirror of https://github.com/streamlink/streamlink
217 lines
7.6 KiB
Python
217 lines
7.6 KiB
Python
import warnings
|
|
from typing import Any, Callable, ClassVar, Dict, Iterator, Mapping, Optional, Sequence, Union
|
|
|
|
from streamlink.exceptions import StreamlinkDeprecationWarning
|
|
|
|
|
|
class Options:
|
|
"""
|
|
For storing options to be used by the Streamlink session and plugins, with default values.
|
|
|
|
Note: Option names are normalized by replacing "_" with "-".
|
|
This means that the keys ``example_one`` and ``example-one`` are equivalent.
|
|
"""
|
|
|
|
_MAP_GETTERS: ClassVar[Mapping[str, Callable[[Any, str], Any]]] = {}
|
|
_MAP_SETTERS: ClassVar[Mapping[str, Callable[[Any, str, Any], None]]] = {}
|
|
|
|
def __init__(self, defaults: Optional[Mapping[str, Any]] = None):
|
|
if not defaults:
|
|
defaults = {}
|
|
|
|
self.defaults = self._normalize_dict(defaults)
|
|
self.options = self.defaults.copy()
|
|
|
|
@staticmethod
|
|
def _normalize_key(name: str) -> str:
|
|
return name.replace("_", "-")
|
|
|
|
@classmethod
|
|
def _normalize_dict(cls, src: Mapping[str, Any]) -> Dict[str, Any]:
|
|
normalize_key = cls._normalize_key
|
|
return {normalize_key(key): value for key, value in src.items()}
|
|
|
|
def clear(self) -> None:
|
|
self.options.clear()
|
|
self.options.update(self.defaults.copy())
|
|
|
|
def get(self, key: str) -> Any:
|
|
normalized = self._normalize_key(key)
|
|
method = self._MAP_GETTERS.get(normalized)
|
|
if method is not None:
|
|
return method(self, normalized)
|
|
else:
|
|
return self.options.get(normalized)
|
|
|
|
def get_explicit(self, key: str) -> Any:
|
|
normalized = self._normalize_key(key)
|
|
return self.options.get(normalized)
|
|
|
|
def set(self, key: str, value: Any) -> None:
|
|
normalized = self._normalize_key(key)
|
|
method = self._MAP_SETTERS.get(normalized)
|
|
if method is not None:
|
|
method(self, normalized, value)
|
|
else:
|
|
self.options[normalized] = value
|
|
|
|
def set_explicit(self, key: str, value: Any) -> None:
|
|
normalized = self._normalize_key(key)
|
|
self.options[normalized] = value
|
|
|
|
def update(self, options: Mapping[str, Any]) -> None:
|
|
for key, value in options.items():
|
|
self.set(key, value)
|
|
|
|
def keys(self):
|
|
return self.options.keys()
|
|
|
|
def values(self):
|
|
return self.options.values()
|
|
|
|
def items(self):
|
|
return self.options.items()
|
|
|
|
def __getitem__(self, item):
|
|
return self.get(item)
|
|
|
|
def __setitem__(self, item, value):
|
|
return self.set(item, value)
|
|
|
|
def __contains__(self, item):
|
|
return self.options.__contains__(item)
|
|
|
|
def __len__(self):
|
|
return self.options.__len__()
|
|
|
|
def __iter__(self):
|
|
return self.options.__iter__()
|
|
|
|
|
|
class Argument:
|
|
"""
|
|
Accepts most of the parameters accepted by :meth:`ArgumentParser.add_argument`,
|
|
except that ``requires`` is a special case which is only enforced if the plugin is in use.
|
|
In addition, the ``name`` parameter is the name relative to the plugin name, but can be overridden by ``argument_name``.
|
|
|
|
Should not be called directly, see the :func:`pluginargument <streamlink.plugin.pluginargument>` decorator.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
required: bool = False,
|
|
requires: Optional[Union[str, Sequence[str]]] = None,
|
|
prompt: Optional[str] = None,
|
|
sensitive: bool = False,
|
|
argument_name: Optional[str] = None,
|
|
dest: Optional[str] = None,
|
|
is_global: bool = False,
|
|
**options,
|
|
):
|
|
"""
|
|
:param name: Argument name, without leading ``--`` or plugin name prefixes, e.g. ``"username"``, ``"password"``, etc.
|
|
:param required: Whether the argument is required for the plugin
|
|
:param requires: List of arguments which this argument requires, eg ``["password"]``
|
|
:param prompt: If the argument is required and not set, this prompt message will be shown instead
|
|
:param sensitive: Whether the argument is sensitive (passwords, etc.) and should be masked
|
|
:param argument_name: Custom CLI argument name without plugin name prefix
|
|
:param dest: Custom plugin option name
|
|
:param is_global: Whether this plugin argument refers to a global CLI argument (deprecated)
|
|
:param options: Arguments passed to :meth:`ArgumentParser.add_argument`, excluding ``requires`` and ``dest``
|
|
"""
|
|
|
|
self.required = required
|
|
self.name = name
|
|
self.options = options
|
|
self._argument_name = self._normalize_name(argument_name) if argument_name else None
|
|
self._dest = self._normalize_dest(dest) if dest else None
|
|
requires = requires or []
|
|
self.requires = list(requires) if isinstance(requires, (list, tuple)) else [requires]
|
|
self.prompt = prompt
|
|
self.sensitive = sensitive
|
|
self._default = options.get("default")
|
|
self.is_global = is_global
|
|
if is_global:
|
|
warnings.warn(
|
|
"Defining global plugin arguments is deprecated. Use the session options instead.",
|
|
StreamlinkDeprecationWarning,
|
|
# set stacklevel to 3 because of the @pluginargument decorator
|
|
# which is the public interface for defining plugin arguments
|
|
stacklevel=3,
|
|
)
|
|
|
|
@staticmethod
|
|
def _normalize_name(name: str) -> str:
|
|
return name.replace("_", "-").strip("-")
|
|
|
|
@staticmethod
|
|
def _normalize_dest(name: str) -> str:
|
|
return name.replace("-", "_")
|
|
|
|
def _name(self, plugin):
|
|
return self._argument_name or self._normalize_name(f"{plugin}-{self.name}")
|
|
|
|
def argument_name(self, plugin):
|
|
return f"--{self.name if self.is_global else self._name(plugin)}"
|
|
|
|
def namespace_dest(self, plugin):
|
|
return self._normalize_dest(self._name(plugin))
|
|
|
|
@property
|
|
def dest(self):
|
|
return self._dest or self._normalize_dest(self.name)
|
|
|
|
@property
|
|
def default(self): # read-only
|
|
return self._default
|
|
|
|
|
|
class Arguments:
|
|
"""
|
|
A collection of :class:`Argument` instances for :class:`Plugin <streamlink.plugin.Plugin>` classes.
|
|
|
|
Should not be called directly, see the :func:`pluginargument <streamlink.plugin.pluginargument>` decorator.
|
|
"""
|
|
|
|
def __init__(self, *args):
|
|
# keep the initial arguments of the constructor in reverse order (see __iter__())
|
|
self.arguments = {arg.name: arg for arg in reversed(args)}
|
|
|
|
def __iter__(self) -> Iterator[Argument]:
|
|
# iterate in reverse order due to add() being called by multiple pluginargument decorators in reverse order
|
|
# TODO: Python 3.7 removal: remove list()
|
|
return reversed(list(self.arguments.values()))
|
|
|
|
def add(self, argument: Argument) -> None:
|
|
self.arguments[argument.name] = argument
|
|
|
|
def get(self, name: str) -> Optional[Argument]:
|
|
return self.arguments.get(name)
|
|
|
|
def requires(self, name: str) -> Iterator[Argument]:
|
|
"""
|
|
Find all :class:`Argument` instances required by name
|
|
"""
|
|
|
|
results = {name}
|
|
argument = self.get(name)
|
|
for reqname in (argument.requires if argument else []):
|
|
required = self.get(reqname)
|
|
if not required:
|
|
raise KeyError(f"{reqname} is not a valid argument for this plugin")
|
|
|
|
if required.name in results:
|
|
raise RuntimeError("cycle detected in plugin argument config")
|
|
results.add(required.name)
|
|
yield required
|
|
|
|
for r in self.requires(required.name):
|
|
if r.name in results:
|
|
raise RuntimeError("cycle detected in plugin argument config")
|
|
results.add(r.name)
|
|
yield r
|
|
|
|
|
|
__all__ = ["Options", "Arguments", "Argument"]
|