mirror of https://github.com/streamlink/streamlink
compat: add deprecated() function
This commit is contained in:
parent
a31b123d36
commit
5ce6cc7a77
|
@ -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",
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue