compat: add deprecated() function

This commit is contained in:
bastimeyer 2024-02-03 00:19:43 +01:00 committed by Sebastian Meyer
parent a31b123d36
commit 5ce6cc7a77
2 changed files with 193 additions and 0 deletions

View File

@ -1,5 +1,11 @@
import importlib
import inspect
import os
import sys
import warnings
from typing import Any, Callable, Dict, Optional, Tuple
from streamlink.exceptions import StreamlinkDeprecationWarning
# compatibility import of charset_normalizer/chardet via requests<3.0
@ -16,7 +22,54 @@ is_win32 = os.name == "nt"
detect_encoding = charset_normalizer.detect
def deprecated(items: Dict[str, Tuple[Optional[str], Any, Any]]) -> None:
"""
Deprecate specific module attributes.
This removes the deprecated attributes from the module's global context,
adds/overrides the module's :func:`__getattr__` function, and emits a :class:`StreamlinkDeprecationWarning`
if one of the deprecated attributes is accessed.
:param items: A mapping of module attribute names to tuples which contain the following optional items:
1. an import path string (for looking up an external object while accessing the attribute)
2. a direct return object (if no import path was set)
3. a custom warning message
"""
mod_globals = inspect.stack()[1].frame.f_globals
orig_getattr: Optional[Callable[[str], Any]] = mod_globals.get("__getattr__", None)
def __getattr__(name: str) -> Any:
if name in items:
origin = f"{mod_globals['__spec__'].name}.{name}"
path, obj, msg = items[name]
warnings.warn(
msg or f"'{origin}' has been deprecated",
StreamlinkDeprecationWarning,
stacklevel=2,
)
if path:
*_path, name = path.split(".")
imported = importlib.import_module(".".join(_path))
obj = getattr(imported, name, None)
return obj
if orig_getattr is not None:
return orig_getattr(name)
raise AttributeError
mod_globals["__getattr__"] = __getattr__
# delete the deprecated module attributes and the imported `deprecated` function
for item in items.keys() | [deprecated.__name__]:
if item in mod_globals:
del mod_globals[item]
__all__ = [
"deprecated",
"detect_encoding",
"is_darwin",
"is_win32",

140
tests/test_compat.py Normal file
View File

@ -0,0 +1,140 @@
import importlib.abc
import importlib.util
from contextlib import nullcontext
from textwrap import dedent
from types import ModuleType
import pytest
from streamlink.exceptions import StreamlinkDeprecationWarning
class TestDeprecated:
class _Loader(importlib.abc.SourceLoader):
def __init__(self, filename: str, content: str):
super().__init__()
self._filename = filename
self._content = content
def get_filename(self, fullname):
return self._filename
def get_data(self, path):
return self._content
@pytest.fixture()
def module(self, request: pytest.FixtureRequest):
content = getattr(request, "param", "")
loader = self._Loader("mocked_module.py", content)
spec = importlib.util.spec_from_loader("mocked_module", loader)
assert spec
mod = importlib.util.module_from_spec(spec)
loader.exec_module(mod)
return mod
@pytest.mark.parametrize(("module", "attr", "has_attr", "warnings", "raises_on_missing"), [
pytest.param(
dedent("""
from streamlink.compat import deprecated
deprecated({
"Streamlink": ("streamlink.session.Streamlink", None, None),
})
""").strip(),
"Streamlink",
False,
[(__file__, StreamlinkDeprecationWarning, "'mocked_module.Streamlink' has been deprecated")],
pytest.raises(AttributeError),
id="import-path",
),
pytest.param(
dedent("""
from streamlink.compat import deprecated
deprecated({
"Streamlink": ("streamlink.session.Streamlink", None, "custom warning"),
})
""").strip(),
"Streamlink",
False,
[(__file__, StreamlinkDeprecationWarning, "custom warning")],
pytest.raises(AttributeError),
id="import-path-custom-msg",
),
pytest.param(
dedent("""
from streamlink.compat import deprecated
from streamlink.session import Streamlink
deprecated({
"Streamlink": (None, Streamlink, None),
})
""").strip(),
"Streamlink",
False,
[(__file__, StreamlinkDeprecationWarning, "'mocked_module.Streamlink' has been deprecated")],
pytest.raises(AttributeError),
id="import-obj",
),
pytest.param(
dedent("""
from streamlink.compat import deprecated
from streamlink.session import Streamlink
deprecated({
"Streamlink": (None, Streamlink, "custom warning"),
})
""").strip(),
"Streamlink",
False,
[(__file__, StreamlinkDeprecationWarning, "custom warning")],
pytest.raises(AttributeError),
id="import-obj-custom-msg",
),
pytest.param(
dedent("""
from streamlink.compat import deprecated
foo = 1
deprecated({
"Streamlink": ("streamlink.session.Streamlink", None, None),
})
""").strip(),
"foo",
True,
[],
pytest.raises(AttributeError),
id="no-warning-has-attr",
),
pytest.param(
dedent("""
from streamlink.compat import deprecated
def __getattr__(name):
return "foo"
deprecated({
"Streamlink": ("streamlink.session.Streamlink", None, None),
})
""").strip(),
"foo",
False,
[],
nullcontext(),
id="no-warning-has-getattr",
),
], indirect=["module"])
def test_deprecated(
self,
recwarn: pytest.WarningsRecorder,
module: ModuleType,
attr: str,
has_attr: bool,
warnings: list,
raises_on_missing: nullcontext,
):
assert recwarn.list == []
assert (attr in dir(module)) is has_attr
assert "deprecated" not in dir(module)
assert getattr(module, attr)
assert [(record.filename, record.category, str(record.message)) for record in recwarn.list] == warnings
with raises_on_missing:
# noinspection PyStatementEffect
module.does_not_exist # noqa: B018