2020-11-08 05:44:23 +01:00
|
|
|
import argparse
|
2018-06-29 20:31:37 +02:00
|
|
|
import unittest
|
2020-11-08 05:44:23 +01:00
|
|
|
from unittest.mock import Mock, patch
|
2012-11-19 03:47:13 +01:00
|
|
|
|
2023-01-07 20:15:46 +01:00
|
|
|
import pytest
|
|
|
|
|
2023-02-03 18:12:03 +01:00
|
|
|
from streamlink.exceptions import StreamlinkDeprecationWarning
|
2020-10-26 14:46:17 +01:00
|
|
|
from streamlink.options import Argument, Arguments, Options
|
2022-08-24 18:54:07 +02:00
|
|
|
from streamlink.plugin import Plugin, pluginargument
|
2022-07-12 18:01:25 +02:00
|
|
|
from streamlink_cli.argparser import ArgumentParser
|
2020-11-08 05:44:23 +01:00
|
|
|
from streamlink_cli.main import setup_plugin_args, setup_plugin_options
|
2012-11-19 03:47:13 +01:00
|
|
|
|
2017-01-16 18:57:12 +01:00
|
|
|
|
2012-11-19 03:47:13 +01:00
|
|
|
class TestOptions(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
|
|
self.options = Options({
|
2018-05-25 23:25:15 +02:00
|
|
|
"a_default": "default",
|
|
|
|
"another-default": "default2",
|
2012-11-19 03:47:13 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
def test_options(self):
|
2023-02-16 00:51:25 +01:00
|
|
|
assert self.options.get("a_default") == "default"
|
|
|
|
assert self.options.get("non_existing") is None
|
2012-11-19 03:47:13 +01:00
|
|
|
|
|
|
|
self.options.set("a_option", "option")
|
2023-02-16 00:51:25 +01:00
|
|
|
assert self.options.get("a_option") == "option"
|
2012-11-19 03:47:13 +01:00
|
|
|
|
2018-06-22 01:15:21 +02:00
|
|
|
def test_options_update(self):
|
2023-02-16 00:51:25 +01:00
|
|
|
assert self.options.get("a_default") == "default"
|
|
|
|
assert self.options.get("non_existing") is None
|
2018-06-22 01:15:21 +02:00
|
|
|
|
|
|
|
self.options.update({"a_option": "option"})
|
2023-02-16 00:51:25 +01:00
|
|
|
assert self.options.get("a_option") == "option"
|
2018-06-22 01:15:21 +02:00
|
|
|
|
2018-05-25 23:25:15 +02:00
|
|
|
def test_options_name_normalised(self):
|
2023-02-16 00:51:25 +01:00
|
|
|
assert self.options.get("a_default") == "default"
|
|
|
|
assert self.options.get("a-default") == "default"
|
|
|
|
assert self.options.get("another-default") == "default2"
|
|
|
|
assert self.options.get("another_default") == "default2"
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
|
2023-01-07 20:15:46 +01:00
|
|
|
class TestMappedOptions:
|
|
|
|
class MappedOptions(Options):
|
|
|
|
def _get_uppercase(self, key):
|
|
|
|
return self.get_explicit(key.upper())
|
|
|
|
|
|
|
|
def _get_add(self, key):
|
|
|
|
return int(self.get_explicit(key)) + 1
|
|
|
|
|
|
|
|
def _set_uppercase(self, key, value):
|
|
|
|
self.set_explicit(key.upper(), value)
|
|
|
|
|
|
|
|
def _set_add(self, key, value):
|
|
|
|
self.set_explicit(key, int(value) + 1)
|
|
|
|
|
|
|
|
_MAP_GETTERS = {
|
|
|
|
"foo-bar": _get_uppercase,
|
|
|
|
"baz": _get_add,
|
|
|
|
}
|
|
|
|
|
|
|
|
_MAP_SETTERS = {
|
|
|
|
"foo-bar": _set_uppercase,
|
|
|
|
"baz": _set_add,
|
|
|
|
}
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
def options(self):
|
|
|
|
return self.MappedOptions({"foo-bar": 123, "baz": 100})
|
|
|
|
|
|
|
|
def test_mapped_key(self, options: MappedOptions):
|
|
|
|
assert options.get("foo-bar") is None
|
|
|
|
assert options.get("foo_bar") is None
|
|
|
|
assert options.get_explicit("foo-bar") == 123
|
|
|
|
assert options.get_explicit("foo_bar") == 123
|
|
|
|
assert options.get_explicit("FOO-BAR") is None
|
|
|
|
assert options.get_explicit("FOO_BAR") is None
|
|
|
|
|
|
|
|
options.set("foo-bar", 321)
|
|
|
|
assert options.get("foo-bar") == 321
|
|
|
|
assert options.get("foo_bar") == 321
|
|
|
|
assert options.get_explicit("foo-bar") == 123
|
|
|
|
assert options.get_explicit("foo_bar") == 123
|
|
|
|
assert options.get_explicit("FOO-BAR") == 321
|
|
|
|
assert options.get_explicit("FOO_BAR") == 321
|
|
|
|
|
|
|
|
def test_mapped_value(self, options: MappedOptions):
|
|
|
|
assert options.get("baz") == 101
|
|
|
|
assert options.get_explicit("baz") == 100
|
|
|
|
|
|
|
|
options.set("baz", 0)
|
|
|
|
assert options.get("baz") == 2
|
|
|
|
assert options.get_explicit("baz") == 1
|
|
|
|
|
2023-02-17 16:52:56 +01:00
|
|
|
def test_mutablemapping_methods(self, options: MappedOptions):
|
|
|
|
options["key"] = "value"
|
|
|
|
assert options["key"] == "value"
|
|
|
|
|
|
|
|
assert options["foo-bar"] is None
|
|
|
|
|
|
|
|
options["baz"] = 0
|
|
|
|
assert options["baz"] == 2
|
|
|
|
|
|
|
|
assert "foo-bar" in options
|
|
|
|
assert "qux" not in options
|
|
|
|
|
|
|
|
assert len(options) == 3
|
|
|
|
|
|
|
|
assert list(iter(options)) == ["foo-bar", "baz", "key"]
|
|
|
|
assert list(options.keys()) == ["foo-bar", "baz", "key"]
|
|
|
|
assert list(options.values()) == [123, 1, "value"]
|
|
|
|
assert list(options.items()) == [("foo-bar", 123), ("baz", 1), ("key", "value")]
|
|
|
|
|
2023-01-07 20:15:46 +01:00
|
|
|
|
2018-05-25 23:25:15 +02:00
|
|
|
class TestArgument(unittest.TestCase):
|
|
|
|
def test_name(self):
|
2023-02-16 00:51:25 +01:00
|
|
|
assert Argument("test-arg").argument_name("plugin") == "--plugin-test-arg"
|
|
|
|
assert Argument("test-arg").namespace_dest("plugin") == "plugin_test_arg"
|
|
|
|
assert Argument("test-arg").dest == "test_arg"
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
def test_name_plugin(self):
|
2023-02-16 00:51:25 +01:00
|
|
|
assert Argument("test-arg").argument_name("test_plugin") == "--test-plugin-test-arg"
|
|
|
|
assert Argument("test-arg").namespace_dest("test_plugin") == "test_plugin_test_arg"
|
|
|
|
assert Argument("test-arg").dest == "test_arg"
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
def test_name_override(self):
|
2023-02-16 00:51:25 +01:00
|
|
|
assert Argument("test", argument_name="override-name").argument_name("plugin") == "--override-name"
|
|
|
|
assert Argument("test", argument_name="override-name").namespace_dest("plugin") == "override_name"
|
|
|
|
assert Argument("test", argument_name="override-name").dest == "test"
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
class TestArguments(unittest.TestCase):
|
|
|
|
def test_getter(self):
|
|
|
|
test1 = Argument("test1")
|
|
|
|
test2 = Argument("test2")
|
|
|
|
args = Arguments(test1, test2)
|
|
|
|
|
2023-02-16 00:51:25 +01:00
|
|
|
assert args.get("test1") == test1
|
|
|
|
assert args.get("test2") == test2
|
|
|
|
assert args.get("test3") is None
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
def test_iter(self):
|
|
|
|
test1 = Argument("test1")
|
|
|
|
test2 = Argument("test2")
|
|
|
|
args = Arguments(test1, test2)
|
|
|
|
|
|
|
|
i_args = iter(args)
|
|
|
|
|
2023-02-16 00:51:25 +01:00
|
|
|
assert next(i_args) == test1
|
|
|
|
assert next(i_args) == test2
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
def test_requires(self):
|
|
|
|
test1 = Argument("test1", requires="test2")
|
|
|
|
test2 = Argument("test2", requires="test3")
|
|
|
|
test3 = Argument("test3")
|
|
|
|
|
|
|
|
args = Arguments(test1, test2, test3)
|
|
|
|
|
2023-02-16 00:51:25 +01:00
|
|
|
assert list(args.requires("test1")) == [test2, test3]
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
def test_requires_invalid(self):
|
|
|
|
test1 = Argument("test1", requires="test2")
|
|
|
|
|
|
|
|
args = Arguments(test1)
|
|
|
|
|
2023-02-16 00:51:25 +01:00
|
|
|
with pytest.raises(KeyError):
|
|
|
|
list(args.requires("test1"))
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
def test_requires_cycle(self):
|
|
|
|
test1 = Argument("test1", requires="test2")
|
|
|
|
test2 = Argument("test2", requires="test1")
|
|
|
|
|
|
|
|
args = Arguments(test1, test2)
|
|
|
|
|
2023-02-16 00:51:25 +01:00
|
|
|
with pytest.raises(RuntimeError):
|
|
|
|
list(args.requires("test1"))
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
def test_requires_cycle_deep(self):
|
|
|
|
test1 = Argument("test1", requires="test-2")
|
|
|
|
test2 = Argument("test-2", requires="test3")
|
|
|
|
test3 = Argument("test3", requires="test1")
|
|
|
|
|
|
|
|
args = Arguments(test1, test2, test3)
|
|
|
|
|
2023-02-16 00:51:25 +01:00
|
|
|
with pytest.raises(RuntimeError):
|
|
|
|
list(args.requires("test1"))
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
def test_requires_cycle_self(self):
|
|
|
|
test1 = Argument("test1", requires="test1")
|
|
|
|
|
|
|
|
args = Arguments(test1)
|
|
|
|
|
2023-02-16 00:51:25 +01:00
|
|
|
with pytest.raises(RuntimeError):
|
|
|
|
list(args.requires("test1"))
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
|
2022-08-24 18:54:07 +02:00
|
|
|
class TestSetupOptions:
|
2023-02-03 18:12:03 +01:00
|
|
|
def test_setup_plugin_args(self, recwarn: pytest.WarningsRecorder):
|
2018-05-25 23:25:15 +02:00
|
|
|
session = Mock()
|
|
|
|
plugin = Mock()
|
2022-07-12 18:01:25 +02:00
|
|
|
parser = ArgumentParser(add_help=False)
|
2020-11-08 05:44:23 +01:00
|
|
|
parser.add_argument("--global-arg1", default=123)
|
|
|
|
parser.add_argument("--global-arg2", default=456)
|
2018-05-25 23:25:15 +02:00
|
|
|
|
|
|
|
session.plugins = {"mock": plugin}
|
|
|
|
plugin.arguments = Arguments(
|
2020-11-08 05:44:23 +01:00
|
|
|
Argument("global-arg1", is_global=True),
|
2018-05-25 23:25:15 +02:00
|
|
|
Argument("test1", default="default1"),
|
|
|
|
Argument("test2", default="default2"),
|
|
|
|
Argument("test3"),
|
|
|
|
)
|
|
|
|
|
2023-02-03 18:12:03 +01:00
|
|
|
assert [(record.category, str(record.message)) for record in recwarn.list] == [
|
|
|
|
(StreamlinkDeprecationWarning, "Defining global plugin arguments is deprecated. Use the session options instead."),
|
|
|
|
]
|
|
|
|
|
2018-05-25 23:25:15 +02:00
|
|
|
setup_plugin_args(session, parser)
|
|
|
|
|
2020-11-14 11:16:12 +01:00
|
|
|
group_plugins = next((grp for grp in parser._action_groups if grp.title == "Plugin options"), None) # pragma: no branch
|
2022-07-12 18:01:25 +02:00
|
|
|
assert group_plugins is not None, "Adds the 'Plugin options' arguments group"
|
|
|
|
assert group_plugins in parser.NESTED_ARGUMENT_GROUPS[None], "Adds the 'Plugin options' arguments group"
|
|
|
|
group_plugin = next((grp for grp in parser._action_groups if grp.title == "Mock"), None) # pragma: no branch
|
|
|
|
assert group_plugin is not None, "Adds the 'Mock' arguments group"
|
|
|
|
assert group_plugin in parser.NESTED_ARGUMENT_GROUPS[group_plugins], "Adds the 'Mock' arguments group"
|
2022-08-24 18:54:07 +02:00
|
|
|
assert [item for action in group_plugin._group_actions for item in action.option_strings] \
|
|
|
|
== ["--mock-test1", "--mock-test2", "--mock-test3"], \
|
2020-11-08 05:44:23 +01:00
|
|
|
"Only adds plugin arguments and ignores global argument references"
|
2022-08-24 18:54:07 +02:00
|
|
|
assert [item for action in parser._actions for item in action.option_strings] \
|
|
|
|
== ["--global-arg1", "--global-arg2", "--mock-test1", "--mock-test2", "--mock-test3"], \
|
2020-11-14 11:16:12 +01:00
|
|
|
"Parser has all arguments registered"
|
2020-11-08 05:44:23 +01:00
|
|
|
|
2022-08-24 18:54:07 +02:00
|
|
|
assert plugin.options.get("global-arg1") == 123
|
|
|
|
assert plugin.options.get("global-arg2") is None
|
|
|
|
assert plugin.options.get("test1") == "default1"
|
|
|
|
assert plugin.options.get("test2") == "default2"
|
|
|
|
assert plugin.options.get("test3") is None
|
2020-11-08 05:44:23 +01:00
|
|
|
|
2023-02-03 18:12:03 +01:00
|
|
|
def test_setup_plugin_options(self, recwarn: pytest.WarningsRecorder):
|
2022-08-24 18:54:07 +02:00
|
|
|
@pluginargument("foo-foo", is_global=True)
|
|
|
|
@pluginargument("bar-bar", default=456)
|
|
|
|
@pluginargument("baz-baz", default=789, help=argparse.SUPPRESS)
|
|
|
|
class FakePlugin(Plugin):
|
|
|
|
def _get_streams(self): # pragma: no cover
|
|
|
|
pass
|
|
|
|
|
2023-03-24 15:33:48 +01:00
|
|
|
assert [(record.category, str(record.message), record.filename) for record in recwarn.list] == [
|
|
|
|
(
|
|
|
|
StreamlinkDeprecationWarning,
|
|
|
|
"Defining global plugin arguments is deprecated. Use the session options instead.",
|
|
|
|
__file__,
|
|
|
|
),
|
2023-02-03 18:12:03 +01:00
|
|
|
]
|
|
|
|
|
2020-11-08 05:44:23 +01:00
|
|
|
session = Mock()
|
2022-07-12 18:01:25 +02:00
|
|
|
parser = ArgumentParser()
|
2020-11-08 05:44:23 +01:00
|
|
|
parser.add_argument("--foo-foo", default=123)
|
|
|
|
|
2022-08-24 18:54:07 +02:00
|
|
|
session.plugins = {"plugin": FakePlugin}
|
2020-11-08 05:44:23 +01:00
|
|
|
session.set_plugin_option = lambda name, key, value: session.plugins[name].options.update({key: value})
|
|
|
|
|
|
|
|
with patch("streamlink_cli.main.args") as args:
|
|
|
|
args.foo_foo = 321
|
|
|
|
args.plugin_bar_bar = 654
|
|
|
|
args.plugin_baz_baz = 987 # this wouldn't be set by the parser if the argument is suppressed
|
|
|
|
|
|
|
|
setup_plugin_args(session, parser)
|
2022-08-24 18:54:07 +02:00
|
|
|
assert FakePlugin.options.get("foo_foo") == 123, "Sets the global-argument's default value"
|
|
|
|
assert FakePlugin.options.get("bar_bar") == 456, "Sets the plugin-argument's default value"
|
|
|
|
assert FakePlugin.options.get("baz_baz") == 789, "Sets the suppressed plugin-argument's default value"
|
|
|
|
|
plugin: remove Plugin.bind()
This changes the way how the Streamlink session and other objects like
the plugin cache and logger are stored on each plugin.
Previously, those objects were set as class attributes on every `Plugin`
class via `Plugin.bind()` when loading plugins via the session's
`load_plugins()` method that gets called on initialization.
This meant that whenever a new Streamlink session was initialized,
references to it (including a dict of every loaded plugin) were set
on each `Plugin` class as a class attribute, and Python's garbage
collector could not get rid of this memory when deleting the session
instance that was created last.
Removing `Plugin.bind()`, passing the session via the `Plugin.__init__`
constructor, and setting the cache, logger, etc. on `Plugin` instances
instead (only one gets initialized by `streamlink_cli`), removes those
static references that prevent the garbage collector to work.
Since the plugin "module" name now doesn't get set via `Plugin.bind()`
anymore, it derives its name via `self.__class__.__module__` on its own,
which means a change of the return type of `Streamlink.resolve_url()`
is necessary in order to pass the plugin name to `streamlink_cli`,
so that it can load config files and initialize plugin arguments, etc.
Breaking changes:
- Remove `Plugin.bind()`
- Pass the `session` instance via the Plugin constructor and set the
`module`, `cache` and `logger` on the plugin instance instead.
Derive `module` from the actual module name.
- Change the return type of `Session.resolve_url()` and include the
resolved plugin name in the returned tuple
Other changes:
- Remove `pluginclass.bind()` call from `Session.load_plugins()` and
use the loader's module name directly on the `Session.plugins` dict
- Remove initialization check from `Plugin` cookie methods
- Update streamlink_cli.main module according to breaking changes
- Update tests respectively
- Add explicit plugin initialization test
- Update tests with plugin constructors and custom plugin names
- Move testplugin override module, so that it shares the same module
name as the main testplugin module. Rel `Session.load_plugins()`
- Refactor most session tests and replace unneeded `resolve_url()`
wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
|
|
|
setup_plugin_options(session, "plugin", FakePlugin)
|
2022-08-24 18:54:07 +02:00
|
|
|
assert FakePlugin.options.get("foo_foo") == 321, "Sets the provided global-argument value"
|
|
|
|
assert FakePlugin.options.get("bar_bar") == 654, "Sets the provided plugin-argument value"
|
|
|
|
assert FakePlugin.options.get("baz_baz") == 789, "Doesn't set values of suppressed plugin-arguments"
|