diff --git a/test/test_compat.py b/test/test_compat.py index 8e40a4180..9b185853d 100644 --- a/test/test_compat.py +++ b/test/test_compat.py @@ -7,6 +7,7 @@ import unittest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from yt_dlp import compat from yt_dlp.compat import ( compat_etree_fromstring, compat_expanduser, @@ -21,6 +22,12 @@ from yt_dlp.compat import ( class TestCompat(unittest.TestCase): + def test_compat_passthrough(self): + with self.assertWarns(DeprecationWarning): + compat.compat_basestring + + compat.asyncio.events # Must not raise error + def test_compat_getenv(self): test_str = 'ั‚ะตัั‚' compat_setenv('yt_dlp_COMPAT_GETENV', test_str) diff --git a/yt_dlp/compat/__init__.py b/yt_dlp/compat/__init__.py index 56a65bb6c..3c395f6d9 100644 --- a/yt_dlp/compat/__init__.py +++ b/yt_dlp/compat/__init__.py @@ -2,11 +2,18 @@ import contextlib import os import subprocess import sys -import types +import warnings import xml.etree.ElementTree as etree from . import re from ._deprecated import * # noqa: F401, F403 +from .compat_utils import passthrough_module + + +# XXX: Implement this the same way as other DeprecationWarnings without circular import +passthrough_module(__name__, '._legacy', callback=lambda attr: warnings.warn( + DeprecationWarning(f'{__name__}.{attr} is deprecated'), stacklevel=2)) +del passthrough_module # HTMLParseError has been deprecated in Python 3.3 and removed in @@ -85,24 +92,3 @@ def windows_enable_vt_mode(): # TODO: Do this the proper way https://bugs.pytho with contextlib.suppress(Exception): subprocess.Popen('', shell=True, startupinfo=startupinfo).wait() WINDOWS_VT_MODE = True - - -class _PassthroughLegacy(types.ModuleType): - def __getattr__(self, attr): - import importlib - with contextlib.suppress(ImportError): - return importlib.import_module(f'.{attr}', __name__) - - legacy = importlib.import_module('._legacy', __name__) - if not hasattr(legacy, attr): - raise AttributeError(f'module {__name__} has no attribute {attr}') - - # XXX: Implement this the same way as other DeprecationWarnings without circular import - import warnings - warnings.warn(DeprecationWarning(f'{__name__}.{attr} is deprecated'), stacklevel=2) - return getattr(legacy, attr) - - -# Python 3.6 does not have module level __getattr__ -# https://peps.python.org/pep-0562/ -sys.modules[__name__].__class__ = _PassthroughLegacy diff --git a/yt_dlp/compat/asyncio/__init__.py b/yt_dlp/compat/asyncio/__init__.py index 0e8c6cad3..21b494499 100644 --- a/yt_dlp/compat/asyncio/__init__.py +++ b/yt_dlp/compat/asyncio/__init__.py @@ -3,6 +3,10 @@ from asyncio import * # noqa: F403 from . import tasks # noqa: F401 +from ..compat_utils import passthrough_module + +passthrough_module(__name__, 'asyncio') +del passthrough_module try: run # >= 3.7 diff --git a/yt_dlp/compat/asyncio/tasks.py b/yt_dlp/compat/asyncio/tasks.py index cb31e52fa..9d98fdfeb 100644 --- a/yt_dlp/compat/asyncio/tasks.py +++ b/yt_dlp/compat/asyncio/tasks.py @@ -2,6 +2,11 @@ from asyncio.tasks import * # noqa: F403 +from ..compat_utils import passthrough_module + +passthrough_module(__name__, 'asyncio.tasks') +del passthrough_module + try: # >= 3.7 all_tasks except NameError: diff --git a/yt_dlp/compat/compat_utils.py b/yt_dlp/compat/compat_utils.py new file mode 100644 index 000000000..938daf926 --- /dev/null +++ b/yt_dlp/compat/compat_utils.py @@ -0,0 +1,44 @@ +import contextlib +import importlib +import sys +import types + + +def _is_package(module): + try: + module.__getattribute__('__path__') + except AttributeError: + return False + return True + + +_NO_ATTRIBUTE = object() + + +def passthrough_module(parent, child, *, callback=lambda _: None): + parent_module = importlib.import_module(parent) + child_module = importlib.import_module(child, parent) + + class PassthroughModule(types.ModuleType): + def __getattr__(self, attr): + if _is_package(parent_module): + with contextlib.suppress(ImportError): + return importlib.import_module(f'.{attr}', parent) + + ret = _NO_ATTRIBUTE + with contextlib.suppress(AttributeError): + ret = getattr(child_module, attr) + + if _is_package(child_module): + with contextlib.suppress(ImportError): + ret = importlib.import_module(f'.{attr}', child) + + if ret is _NO_ATTRIBUTE: + raise AttributeError(f'module {parent} has no attribute {attr}') + + callback(attr) + return ret + + # Python 3.6 does not have module level __getattr__ + # https://peps.python.org/pep-0562/ + sys.modules[parent].__class__ = PassthroughModule diff --git a/yt_dlp/compat/re.py b/yt_dlp/compat/re.py index e8a6fabbd..d4532950a 100644 --- a/yt_dlp/compat/re.py +++ b/yt_dlp/compat/re.py @@ -2,6 +2,11 @@ from re import * # F403 +from .compat_utils import passthrough_module + +passthrough_module(__name__, 're') +del passthrough_module + try: Pattern # >= 3.7 except NameError: