From 5989c90dd32aaf11343d6f8494e33610974fc9d4 Mon Sep 17 00:00:00 2001 From: Billy2011 Date: Mon, 29 Aug 2022 14:01:55 +0200 Subject: [PATCH] [PATCH] docs: add plugin{matcher,argument} docstrings(#4771) - Add docstrings for the `pluginmatcher` and `pluginargument` decorators - Update typing information of `Plugin.arguments` and add docstring - Update docstrings of `Argument` and `Arguments` - Fix minor issue in `Arguments.requires()` --- docs/api.rst | 13 +++- src/streamlink/options.py | 57 +++++++----------- src/streamlink/plugin/plugin.py | 103 ++++++++++++++++++++++++++------ 3 files changed, 118 insertions(+), 55 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index cdf64087cc5..53f8bedd7e8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -22,7 +22,18 @@ Plugins ------- .. module:: streamlink.plugin .. autoclass:: Plugin - :members: + :private-members: _get_streams + :member-order: bysource + +Plugin decorators +^^^^^^^^^^^^^^^^^ + +.. autodecorator:: pluginmatcher + +.. autodecorator:: pluginargument + +Plugin arguments +^^^^^^^^^^^^^^^^ .. module:: streamlink.options .. autoclass:: Arguments diff --git a/src/streamlink/options.py b/src/streamlink/options.py index 7e226df0270..21d45220f57 100644 --- a/src/streamlink/options.py +++ b/src/streamlink/options.py @@ -46,24 +46,25 @@ def update(self, options): class Argument(object): """ - :class:`Argument` accepts most of the same parameters as :func:`ArgumentParser.add_argument`, - except requires is a special case as in this case it is only enforced if the plugin is in use. - In addition the name parameter is the name relative to the plugin eg. username, password, etc. - + 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 ` decorator. """ + def __init__(self, name, required=False, requires=None, prompt=None, sensitive=False, argument_name=None, dest=None, is_global=False, **options): """ - :param name: name of the argument, without -- or plugin name prefixes, eg. ``"password"``, ``"mux-subtitles"``, etc. - :param required (bool): if the argument is required for the plugin - :param requires: list of the arguments which this argument requires, eg ``["password"]`` - :param prompt: if the argument is required and not given, this prompt will show at run time - :param sensitive (bool): if the argument is sensitive (passwords, etc) and should be masked in logs and if - prompted use askpass - :param argument_name: - :param option_name: - :param options: arguments passed to :func:`ArgumentParser.add_argument`, excluding requires, and dest + :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 + :param options: Arguments passed to :meth:`ArgumentParser.add_argument`, excluding ``requires`` and ``dest`` """ self.required = required self.name = name @@ -97,26 +98,11 @@ def default(self): # read-only class Arguments(object): """ - Provides a wrapper around a list of :class:`Argument`. For example - - .. code-block:: python - - class PluginExample(Plugin): - arguments = PluginArguments( - PluginArgument("username", - help="The username for your account.", - metavar="EMAIL", - requires=["password"]), // requires the password too - PluginArgument("password", - sensitive=True, // should be masked in logs, etc. - help="The password for your account.", - metavar="PASSWORD") - ) - - This will add the ``--plugin-username`` and ``--plugin-password`` arguments to the CLI - (assuming the plugin module is ``plugin``). + A collection of :class:`Argument` instances for :class:`Plugin ` classes. + Should not be called directly, see the :func:`pluginargument ` decorator. """ + def __init__(self, *args): # keep the initial arguments of the constructor in reverse order (see __iter__()) self.arguments = OrderedDict((arg.name, arg) for arg in reversed(args)) @@ -136,16 +122,13 @@ def get(self, name): return self.arguments.get(name) def requires(self, name): + # type: (str) -> Iterator[Argument] """ - Find all the arguments required by name - - :param name: name of the argument the find the dependencies - - :return: list of dependant arguments + Find all :class:`Argument` instances required by name """ results = {name} argument = self.get(name) - for reqname in argument.requires: + for reqname in (argument.requires if argument else []): required = self.get(reqname) if not required: raise KeyError("{0} is not a valid argument for this plugin".format(reqname)) diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py index 74556d9da70..db0aa8d275e 100644 --- a/src/streamlink/plugin/plugin.py +++ b/src/streamlink/plugin/plugin.py @@ -6,7 +6,7 @@ from collections import OrderedDict, namedtuple from functools import partial from http.cookiejar import Cookie -from typing import Any, Callable, Dict, List, Optional, Pattern, Sequence, Type, Union +from typing import Any, Callable, ClassVar, Dict, List, Optional, Pattern, Sequence, Type, Union import requests.cookies @@ -166,37 +166,47 @@ def parse_params(params=None): class Plugin(object): - """A plugin can retrieve stream information from the URL specified. + """ + Plugin base class for retrieving streams and metadata from the URL specified. + """ + + matchers = None # type: ClassVar[Optional[List[Matcher]]] + """ + The list of plugin matchers (URL pattern + priority). + + Use the :func:`pluginmatcher` decorator to initialize this list. + """ + + arguments = None # type: ClassVar[Optional[Arguments]] + """ + The plugin's :class:`Arguments ` collection. - :param url: URL that the plugin will operate on + Use the :func:`pluginargument` decorator to initialize this collection. """ + matchers = None # type: ClassVar[List[Matcher]] # the list of plugin matchers (URL pattern + priority) # use the streamlink.plugin.pluginmatcher decorator for initializing this list - # matchers: ClassVar[List[Matcher]] = None - matchers = None - # a tuple of `re.Match` results of all defined matchers + # matches: Sequence[Optional[Match]] - # a reference to the compiled `re.Pattern` of the first matching matcher + # a tuple of `re.Match` results of all defined matchers + # matcher: Pattern - # a reference to the `re.Match` result of the first matching matcher + # a reference to the compiled `re.Pattern` of the first matching matcher + # match: Match + # a reference to the `re.Match` result of the first matching matcher # plugin metadata attributes - id = None - # type: Optional[str] - author = None - # type: Optional[str] - category = None - # type: Optional[str] - title = None - # type: Optional[str] + id = None # type: Optional[str] + author = None # type: Optional[str] + category = None # type: Optional[str] + title = None # type: Optional[str] cache = None logger = None module = "unknown" options = Options() - arguments = None # type: Optional[Arguments] session = None _url = None # type: Optional[str] @@ -554,6 +564,38 @@ def input_ask_password(self, prompt): def pluginmatcher(pattern, priority=NORMAL_PRIORITY): # type: (Pattern, int) -> Callable[[Type[Plugin]], Type[Plugin]] + """ + Decorator for plugin URL matchers. + + A matcher consists of a compiled regular expression pattern for the plugin's input URL and a priority value. + The priority value determines which plugin gets chosen by + :meth:`Streamlink.resolve_url ` if multiple plugins match the input URL. + + Plugins must at least have one matcher. If multiple matchers are defined, then the first matching one + according to the order of which they have been defined (top to bottom) will be responsible for setting the + :attr:`Plugin.matcher` and :attr:`Plugin.match` attributes on the :class:`Plugin` instance. + The :attr:`Plugin.matchers` and :attr:`Plugin.matches` attributes are affected by all defined matchers. + + .. code-block:: python + + import re + + from streamlink.plugin import HIGH_PRIORITY, Plugin, pluginmatcher + + + @pluginmatcher(re.compile("https?://example:1234/(?:foo|bar)/(?P[^/]+)")) + @pluginmatcher(priority=HIGH_PRIORITY, pattern=re.compile(\"\"\" + https?://(?: + sitenumberone + |adifferentsite + |somethingelse + ) + /.+\\.m3u8 + \"\"\", re.VERBOSE)) + class MyPlugin(Plugin): + ... + """ + matcher = Matcher(pattern, priority) def decorator(cls): @@ -581,6 +623,33 @@ def pluginargument( **options ): # type: () -> Callable[[Type[Plugin]], Type[Plugin]] + """ + Decorator for plugin arguments. Takes the same arguments as :class:`streamlink.options.Argument`. + + .. code-block:: python + + from streamlink.plugin import Plugin, pluginargument + + + @pluginargument( + "username", + requires=["password"], + metavar="EMAIL", + help="The username for your account.", + ) + @pluginargument( + "password", + sensitive=True, + metavar="PASSWORD", + help="The password for your account.", + ) + class MyPlugin(Plugin): + ... + + This will add the ``--myplugin-username`` and ``--myplugin-password`` arguments to the CLI, + assuming the plugin's module name is ``myplugin``. + """ + arg = Argument( name, required=required,