task: metadata: install: pip install -e .[dev]
patch: diff --git a/docs/install.rst b/docs/install.rst
index e1188aa0..99059a57 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -341,16 +341,16 @@ Release                              Notes
                                      See the `commit log`_ for a list of changes since the last stable release.
 ==================================== ====================================
 
-.. note::
+.. warning::
 
-    **The Streamlink installer for Windows is currently based on Python 3.8.** |br|
-    Versions of Windows prior to 7 are not supported.
+    **The Streamlink installer for Windows is currently based on Python 3.9.** |br|
+    Versions of Windows prior to 10 are **not** supported.
 
     Be aware that the packages for `Chocolatey`_ and the `Windows Package Manager`_ are just wrappers
-    around the stable installer and thus depend on Windows 7+ as well.
+    around the stable installer and thus depend on Windows 10+ as well.
 
     Alternatively, :ref:`Streamlink can be installed via python-pip <install:PyPI package and source code>`
-    in a :ref:`compatible <install:Dependencies>` Python environment.
+    in a :ref:`compatible Python environment <install:Dependencies>`.
 
 .. _Stable release:
 .. _GitHub releases page: https://github.com/streamlink/streamlink/releases/latest
diff --git a/script/makeinstaller.sh b/script/makeinstaller.sh
index 9825a2f5..03b1f6e0 100755
--- a/script/makeinstaller.sh
+++ b/script/makeinstaller.sh
@@ -33,7 +33,7 @@ cd "${ROOT}"
 STREAMLINK_VERSION=$(python setup.py --version)
 STREAMLINK_VERSION_PLAIN="${STREAMLINK_VERSION%%+*}"
 STREAMLINK_INSTALLER="${1:-"streamlink-${STREAMLINK_VERSION/\+/_}"}"
-STREAMLINK_PYTHON_VERSION=3.8.7
+STREAMLINK_PYTHON_VERSION=3.9.6
 STREAMLINK_ASSETS_REPO="${STREAMLINK_ASSETS_REPO:-streamlink/streamlink-assets}"
 STREAMLINK_ASSETS_RELEASE="${STREAMLINK_ASSETS_RELEASE:-latest}"
 
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
index 6334ed41..1b789ddc 100644
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -480,7 +480,7 @@ class Plugin:
         :return: list of the restored cookie names
         """
         if not self.session or not self.cache:
-            raise RuntimeError("Cannot loaded cached cookies in unbound plugin")
+            raise RuntimeError("Cannot load cached cookies in unbound plugin")
 
         restored = []
 
@@ -504,7 +504,7 @@ class Plugin:
         :return: list of the removed cookie names
         """
         if not self.session or not self.cache:
-            raise RuntimeError("Cannot loaded cached cookies in unbound plugin")
+            raise RuntimeError("Cannot clear cached cookies in unbound plugin")
 
         cookie_filter = cookie_filter or (lambda c: True)
         removed = []

problem_statement: Tests are failing on test_cookie_clear_unbound
### Checklist

- [X] This is a bug report and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest stable release

### Description

I'm trying to build the package for 2.3.0 on Arch and the tests are failing on test_cookie_clear_unbound. This does look to be an issue with the test itself. I have attached the build log.

[streamlink-2.3.0-1-x86_64-check.log](https://github.com/streamlink/streamlink/files/7001827/streamlink-2.3.0-1-x86_64-check.log)


### Debug log

```text
This issue is on the build process.
```

repo: streamlink/streamlink
test_cmd: pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
test_patch: diff --git a/tests/plugins/test_dash.py b/tests/plugins/test_dash.py
index d7dc082d..5baf49fa 100644
--- a/tests/plugins/test_dash.py
+++ b/tests/plugins/test_dash.py
@@ -3,7 +3,7 @@ from unittest.mock import Mock, patch
 
 import pytest
 
-from streamlink.plugin.plugin import BIT_RATE_WEIGHT_RATIO, LOW_PRIORITY, NORMAL_PRIORITY, NO_PRIORITY, Plugin
+from streamlink.plugin.plugin import BIT_RATE_WEIGHT_RATIO, LOW_PRIORITY, NORMAL_PRIORITY, NO_PRIORITY
 from streamlink.plugins.dash import MPEGDASH
 from tests.plugins import PluginCanHandleUrl
 
@@ -26,7 +26,7 @@ class TestPluginCanHandleUrlMPEGDASH(PluginCanHandleUrl):
 ])
 def test_priority(url, priority):
     session = Mock()
-    Plugin.bind(session, "tests.plugins.test_dash")
+    MPEGDASH.bind(session, "tests.plugins.test_dash")
     assert next((matcher.priority for matcher in MPEGDASH.matchers if matcher.pattern.match(url)), NO_PRIORITY) == priority
 
 
@@ -41,7 +41,7 @@ class TestPluginMPEGDASH(unittest.TestCase):
     @patch("streamlink.stream.DASHStream.parse_manifest")
     def test_get_streams_prefix(self, parse_manifest):
         session = Mock()
-        Plugin.bind(session, "tests.plugins.test_dash")
+        MPEGDASH.bind(session, "tests.plugins.test_dash")
         p = MPEGDASH("dash://http://example.com/foo.mpd")
         p.streams()
         parse_manifest.assert_called_with(session, "http://example.com/foo.mpd")
@@ -49,7 +49,7 @@ class TestPluginMPEGDASH(unittest.TestCase):
     @patch("streamlink.stream.DASHStream.parse_manifest")
     def test_get_streams(self, parse_manifest):
         session = Mock()
-        Plugin.bind(session, "tests.plugins.test_dash")
+        MPEGDASH.bind(session, "tests.plugins.test_dash")
         p = MPEGDASH("http://example.com/foo.mpd")
         p.streams()
         parse_manifest.assert_called_with(session, "http://example.com/foo.mpd")
diff --git a/tests/plugins/test_filmon.py b/tests/plugins/test_filmon.py
index 93a9e770..2deac646 100644
--- a/tests/plugins/test_filmon.py
+++ b/tests/plugins/test_filmon.py
@@ -2,7 +2,6 @@ from unittest.mock import Mock
 
 import pytest
 
-from streamlink.plugin import Plugin
 from streamlink.plugins.filmon import Filmon
 from tests.plugins import PluginCanHandleUrl
 
@@ -35,7 +34,7 @@ class TestPluginCanHandleUrlFilmon(PluginCanHandleUrl):
     ('http://www.filmon.tv/vod/view/10250-0-crime-boss', [None, None, '10250'])
 ])
 def test_match_url(url, expected):
-    Plugin.bind(Mock(), "tests.plugins.test_filmon")
+    Filmon.bind(Mock(), "tests.plugins.test_filmon")
     plugin = Filmon(url)
     assert plugin.match is not None
     # expected must return [is_group, channel, vod_id]
diff --git a/tests/plugins/test_qq.py b/tests/plugins/test_qq.py
index c32b2c80..7985e119 100644
--- a/tests/plugins/test_qq.py
+++ b/tests/plugins/test_qq.py
@@ -2,7 +2,6 @@ from unittest.mock import Mock
 
 import pytest
 
-from streamlink.plugin import Plugin
 from streamlink.plugins.qq import QQ
 from tests.plugins import PluginCanHandleUrl
 
@@ -31,7 +30,7 @@ class TestPluginCanHandleUrlQQ(PluginCanHandleUrl):
     ("http://m.live.qq.com/10039165", "room_id", "10039165")
 ])
 def test_match_url(url, group, expected):
-    Plugin.bind(Mock(), "tests.plugins.test_qq")
+    QQ.bind(Mock(), "tests.plugins.test_qq")
     plugin = QQ(url)
     assert plugin.match is not None
     assert plugin.match.group(group) == expected
diff --git a/tests/plugins/test_youtube.py b/tests/plugins/test_youtube.py
index f05e070d..ea14da4c 100644
--- a/tests/plugins/test_youtube.py
+++ b/tests/plugins/test_youtube.py
@@ -2,7 +2,6 @@ from unittest.mock import Mock
 
 import pytest
 
-from streamlink.plugin import Plugin
 from streamlink.plugins.youtube import YouTube
 from tests.plugins import PluginCanHandleUrl
 
@@ -54,7 +53,7 @@ class TestPluginCanHandleUrlYouTube(PluginCanHandleUrl):
     ("https://www.youtube.com/watch?v=aqz-KE-bpKQ", "video_id", "aqz-KE-bpKQ"),
 ])
 def test_match_url(url, group, expected):
-    Plugin.bind(Mock(), "tests.plugins.test_youtube")
+    YouTube.bind(Mock(), "tests.plugins.test_youtube")
     plugin = YouTube(url)
     assert plugin.match is not None
     assert plugin.match.group(group) == expected
@@ -68,5 +67,5 @@ def test_match_url(url, group, expected):
     ("http://www.youtube.com/watch?v=0123456789A", "https://www.youtube.com/watch?v=0123456789A"),
 ])
 def test_translate_url(url, expected):
-    Plugin.bind(Mock(), "tests.plugins.test_youtube")
+    YouTube.bind(Mock(), "tests.plugins.test_youtube")
     assert YouTube(url).url == expected
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 038b752e..3d1f70e3 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -132,15 +132,21 @@ class TestPlugin(unittest.TestCase):
 
     def test_cookie_load_unbound(self):
         plugin = Plugin("http://test.se")
-        self.assertRaises(RuntimeError, plugin.load_cookies)
+        with self.assertRaises(RuntimeError) as cm:
+            plugin.load_cookies()
+        self.assertEqual(str(cm.exception), "Cannot load cached cookies in unbound plugin")
 
     def test_cookie_save_unbound(self):
         plugin = Plugin("http://test.se")
-        self.assertRaises(RuntimeError, plugin.save_cookies)
+        with self.assertRaises(RuntimeError) as cm:
+            plugin.save_cookies()
+        self.assertEqual(str(cm.exception), "Cannot cache cookies in unbound plugin")
 
     def test_cookie_clear_unbound(self):
         plugin = Plugin("http://test.se")
-        self.assertRaises(RuntimeError, plugin.clear_cookies)
+        with self.assertRaises(RuntimeError) as cm:
+            plugin.clear_cookies()
+        self.assertEqual(str(cm.exception), "Cannot clear cached cookies in unbound plugin")
 
 
 class TestPluginMatcher(unittest.TestCase):