Skip to content

Commit

Permalink
[PATCH] docs: add plugin{matcher,argument} docstrings(streamlink#4771)
Browse files Browse the repository at this point in the history
- 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()`
  • Loading branch information
Billy2011 committed Aug 29, 2022
1 parent 90abf7e commit 5989c90
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 55 deletions.
13 changes: 12 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 20 additions & 37 deletions src/streamlink/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <streamlink.plugin.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
Expand Down Expand Up @@ -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 <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 = OrderedDict((arg.name, arg) for arg in reversed(args))
Expand All @@ -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))
Expand Down
103 changes: 86 additions & 17 deletions src/streamlink/plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <streamlink.options.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]

Expand Down Expand Up @@ -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 <streamlink.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<name>[^/]+)"))
@pluginmatcher(priority=HIGH_PRIORITY, pattern=re.compile(\"\"\"
https?://(?:
sitenumberone
|adifferentsite
|somethingelse
)
/.+\\.m3u8
\"\"\", re.VERBOSE))
class MyPlugin(Plugin):
...
"""

matcher = Matcher(pattern, priority)

def decorator(cls):
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 5989c90

Please sign in to comment.