Skip to content

Commit

Permalink
plugin.api.http_session: remove parse_* methods
Browse files Browse the repository at this point in the history
The `parse_{cookies,headers,query_params}` methods were added when the
subclass of `requests.Session` was implemented in order to support
setting cookies, headers and query parameters via `k1=v1;k2=v2` strings
(in addition to key-value dicts) via the session API and via the CLI:
- 936e66d
- c6e54fd

Since these methods implement logic purely for the `Streamlink` session
interface and are not meant to be called by any plugin or stream
implementations which use the session's `HTTPSession` instance, they
should be removed. Cookies, headers and query string parameters should
be set directly on their respective `HTTPSession` attributes:
- `cookies`: instance of `requests.cookies.RequestsCookieJar`
- `headers`: instance of `requests.structures.CaseInsensitiveDict`
- `params`: instance of `dict`

Also, at least in regards to HTTP headers, the `key=value` syntax
does not reflect the syntax of raw HTTP requests/responses or interfaces
of other tools like cURL, etc., so having these methods on the
`HTTPSession` class makes it unnecessarily confusing. The method names
themselves are also confusing, as they suggest that the input gets
parsed and that some result gets returned, which is wrong.

This commit therefore moves the `k1=v1;k2=v2` string logic from the
`http_session` module to the `session` module where it belongs and it
also simplifies the option setter.
  • Loading branch information
bastimeyer committed Sep 5, 2022
1 parent 8a7717f commit 3392416
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 56 deletions.
33 changes: 0 additions & 33 deletions src/streamlink/plugin/api/http_session.py
Expand Up @@ -80,15 +80,6 @@ def __len__(self) -> int:
urllib3.util.url.PERCENT_RE = Urllib3UtilUrlPercentReOverride # type: ignore[attr-defined]


def _parse_keyvalue_list(val):
for keyvalue in val.split(";"):
try:
key, value = keyvalue.split("=", 1)
yield key.strip(), value.strip()
except ValueError:
continue


# requests.Request.__init__ keywords, except for "hooks"
_VALID_REQUEST_ARGS = "method", "url", "headers", "files", "data", "params", "auth", "cookies", "json"

Expand Down Expand Up @@ -139,30 +130,6 @@ def xml(cls, res, *args, **kwargs):
"""Parses XML from a response."""
return parse_xml(res.text, *args, **kwargs)

def parse_cookies(self, cookies, **kwargs):
"""Parses a semi-colon delimited list of cookies.
Example: foo=bar;baz=qux
"""
for name, value in _parse_keyvalue_list(cookies):
self.cookies.set(name, value, **kwargs)

def parse_headers(self, headers):
"""Parses a semi-colon delimited list of headers.
Example: foo=bar;baz=qux
"""
for name, value in _parse_keyvalue_list(headers):
self.headers[name] = value

def parse_query_params(self, cookies, **kwargs):
"""Parses a semi-colon delimited list of query parameters.
Example: foo=bar;baz=qux
"""
for name, value in _parse_keyvalue_list(cookies):
self.params[name] = value

def resolve_url(self, url):
"""Resolves any redirects and returns the final URL."""
return self.get(url, stream=True).url
Expand Down
35 changes: 17 additions & 18 deletions src/streamlink/session.py
Expand Up @@ -2,7 +2,7 @@
import pkgutil
from functools import lru_cache
from socket import AF_INET, AF_INET6
from typing import Any, Dict, Optional, Tuple, Type
from typing import Any, Dict, Iterator, Optional, Tuple, Type

# noinspection PyPackageRequirements
import urllib3.util.connection as urllib3_util_connection
Expand All @@ -27,6 +27,18 @@
# noinspection PyUnresolvedReferences
_original_allowed_gai_family = urllib3_util_connection.allowed_gai_family # type: ignore[attr-defined]

# options which support `key1=value1;key2=value2;...` strings as value
_OPTIONS_HTTP_KEYEQUALSVALUE = {"http-cookies": "cookies", "http-headers": "headers", "http-query-params": "params"}


def _parse_keyvalue_string(value: str) -> Iterator[Tuple[str, str]]:
for keyval in value.split(";"):
try:
key, val = keyval.split("=", 1)
yield key.strip(), val.strip()
except ValueError:
continue


class PythonDeprecatedWarning(UserWarning):
pass
Expand Down Expand Up @@ -235,23 +247,10 @@ def set_option(self, key: str, value: Any):
if key == "https-proxy":
log.warning("The https-proxy option has been deprecated in favor of a single http-proxy option")

elif key == "http-cookies":
if isinstance(value, dict):
self.http.cookies.update(value)
else:
self.http.parse_cookies(value)

elif key == "http-headers":
if isinstance(value, dict):
self.http.headers.update(value)
else:
self.http.parse_headers(value)

elif key == "http-query-params":
if isinstance(value, dict):
self.http.params.update(value)
else:
self.http.parse_query_params(value)
elif key in _OPTIONS_HTTP_KEYEQUALSVALUE:
getattr(self.http, _OPTIONS_HTTP_KEYEQUALSVALUE[key]).update(
value if isinstance(value, dict) else dict(_parse_keyvalue_string(value))
)

elif key == "http-trust-env":
self.http.trust_env = value
Expand Down
35 changes: 30 additions & 5 deletions tests/test_session.py
Expand Up @@ -23,6 +23,12 @@
_original_allowed_gai_family = urllib3.util.connection.allowed_gai_family # type: ignore[attr-defined]


@pytest.fixture
def session():
with patch("streamlink.session.Streamlink.load_builtin_plugins"):
yield Streamlink()


class EmptyPlugin(Plugin):
def _get_streams(self):
pass # pragma: no cover
Expand Down Expand Up @@ -409,11 +415,6 @@ def test_http_disable_dh(self, mock_urllib3_util_ssl):


class TestSessionOptionHttpProxy:
@pytest.fixture
def session(self):
with patch("streamlink.session.Streamlink.load_builtin_plugins"):
yield Streamlink()

@pytest.fixture
def no_deprecation(self, caplog: pytest.LogCaptureFixture):
yield
Expand Down Expand Up @@ -463,3 +464,27 @@ def test_https_proxy_socks(self, session: Streamlink, logs_deprecation):

assert session.http.proxies["http"] == "socks5://localhost:1234"
assert session.http.proxies["https"] == "socks5://localhost:1234"


@pytest.mark.parametrize("option", [
pytest.param(("http-cookies", "cookies"), id="http-cookies"),
pytest.param(("http-headers", "headers"), id="http-headers"),
pytest.param(("http-query-params", "params"), id="http-query-params"),
], indirect=True)
class TestOptionsKeyEqualsValue:
@pytest.fixture
def option(self, request, session: Streamlink):
option, attr = request.param
httpsessionattr = getattr(session.http, attr)
assert session.get_option(option) is httpsessionattr
assert "foo" not in httpsessionattr
assert "bar" not in httpsessionattr
yield option
assert httpsessionattr.get("foo") == "foo=bar"
assert httpsessionattr.get("bar") == "123"

def test_dict(self, session: Streamlink, option: str):
session.set_option(option, {"foo": "foo=bar", "bar": "123"})

def test_string(self, session: Streamlink, option: str):
session.set_option(option, "foo=foo=bar;bar=123;baz")

0 comments on commit 3392416

Please sign in to comment.