streamlink/src/streamlink/options.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

217 lines
7.6 KiB
Python
Raw Normal View History

import warnings
from typing import Any, Callable, ClassVar, Dict, Iterator, Mapping, Optional, Sequence, Union
from streamlink.exceptions import StreamlinkDeprecationWarning
2012-08-23 22:46:06 +02:00
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,
2022-05-21 00:39:06 +02:00
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
2022-05-21 00:39:06 +02:00
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"]