streamlink/tests/test_options.py

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

274 lines
10 KiB
Python
Raw Normal View History

import argparse
import unittest
from unittest.mock import Mock, patch
2012-11-19 03:47:13 +01:00
import pytest
from streamlink.exceptions import StreamlinkDeprecationWarning
from streamlink.options import Argument, Arguments, Options
2022-08-24 18:54:07 +02:00
from streamlink.plugin import Plugin, pluginargument
from streamlink_cli.argparser import ArgumentParser
from streamlink_cli.main import setup_plugin_args, setup_plugin_options
2012-11-19 03:47:13 +01:00
2012-11-19 03:47:13 +01:00
class TestOptions(unittest.TestCase):
def setUp(self):
self.options = Options({
"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
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
self.options.update({"a_option": "option"})
2023-02-16 00:51:25 +01:00
assert self.options.get("a_option") == "option"
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"
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
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")]
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"
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"
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"
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
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
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]
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"))
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"))
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"))
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"))
2022-08-24 18:54:07 +02:00
class TestSetupOptions:
def test_setup_plugin_args(self, recwarn: pytest.WarningsRecorder):
session = Mock()
plugin = Mock()
parser = ArgumentParser(add_help=False)
parser.add_argument("--global-arg1", default=123)
parser.add_argument("--global-arg2", default=456)
session.plugins = {"mock": plugin}
plugin.arguments = Arguments(
Argument("global-arg1", is_global=True),
Argument("test1", default="default1"),
Argument("test2", default="default2"),
Argument("test3"),
)
assert [(record.category, str(record.message)) for record in recwarn.list] == [
(StreamlinkDeprecationWarning, "Defining global plugin arguments is deprecated. Use the session options instead."),
]
setup_plugin_args(session, parser)
group_plugins = next((grp for grp in parser._action_groups if grp.title == "Plugin options"), None) # pragma: no branch
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"], \
"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"], \
"Parser has all arguments registered"
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
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
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__,
),
]
session = Mock()
parser = ArgumentParser()
parser.add_argument("--foo-foo", default=123)
2022-08-24 18:54:07 +02:00
session.plugins = {"plugin": FakePlugin}
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"