]> jfr.im git - yt-dlp.git/blame - yt_dlp/compat/compat_utils.py
[compat_utils] Improve `passthrough_module`
[yt-dlp.git] / yt_dlp / compat / compat_utils.py
CommitLineData
1d485a1a 1import collections
9196cbfe 2import contextlib
88426d94 3import functools
9196cbfe 4import importlib
5import sys
6import types
7
1d485a1a 8_NO_ATTRIBUTE = object()
9
10_Package = collections.namedtuple('Package', ('name', 'version'))
11
12
13def get_package_info(module):
f6a765ce 14 return _Package(
15 name=getattr(module, '_yt_dlp__identifier', module.__name__),
16 version=str(next(filter(None, (
17 getattr(module, attr, None)
18 for attr in ('__version__', 'version_string', 'version')
19 )), None)))
1d485a1a 20
21
9196cbfe 22def _is_package(module):
754c84e2 23 return '__path__' in vars(module)
24
25
88426d94 26def _is_dunder(name):
27 return name.startswith('__') and name.endswith('__')
28
29
754c84e2 30class EnhancedModule(types.ModuleType):
31 def __new__(cls, name, *args, **kwargs):
32 if name not in sys.modules:
33 return super().__new__(cls, name, *args, **kwargs)
34
35 assert not args and not kwargs, 'Cannot pass additional arguments to an existing module'
36 module = sys.modules[name]
37 module.__class__ = cls
38 return module
39
40 def __init__(self, name, *args, **kwargs):
41 # Prevent __new__ from trigerring __init__ again
42 if name not in sys.modules:
43 super().__init__(name, *args, **kwargs)
44
45 def __bool__(self):
46 return vars(self).get('__bool__', lambda: True)()
47
48 def __getattribute__(self, attr):
49 try:
50 ret = super().__getattribute__(attr)
51 except AttributeError:
88426d94 52 if _is_dunder(attr):
754c84e2 53 raise
54 getter = getattr(self, '__getattr__', None)
55 if not getter:
56 raise
57 ret = getter(attr)
58 return ret.fget() if isinstance(ret, property) else ret
9196cbfe 59
60
88426d94 61def passthrough_module(parent, child, allowed_attributes=(..., ), *, callback=lambda _: None):
754c84e2 62 """Passthrough parent module into a child module, creating the parent if necessary"""
63 parent = EnhancedModule(parent)
3c5386cd 64
754c84e2 65 def __getattr__(attr):
66 if _is_package(parent):
67 with contextlib.suppress(ImportError):
68 return importlib.import_module(f'.{attr}', parent.__name__)
560738f3 69
754c84e2 70 ret = from_child(attr)
71 if ret is _NO_ATTRIBUTE:
72 raise AttributeError(f'module {parent.__name__} has no attribute {attr}')
73 callback(attr)
74 return ret
9196cbfe 75
88426d94 76 @functools.lru_cache(maxsize=None)
754c84e2 77 def from_child(attr):
78 nonlocal child
88426d94 79 if attr not in allowed_attributes:
80 if ... not in allowed_attributes or _is_dunder(attr):
754c84e2 81 return _NO_ATTRIBUTE
9196cbfe 82
754c84e2 83 if isinstance(child, str):
84 child = importlib.import_module(child, parent.__name__)
85
754c84e2 86 if _is_package(child):
87 with contextlib.suppress(ImportError):
88426d94 88 return passthrough_module(f'{parent.__name__}.{attr}',
89 importlib.import_module(f'.{attr}', child.__name__))
90
91 with contextlib.suppress(AttributeError):
92 return getattr(child, attr)
754c84e2 93
94 return _NO_ATTRIBUTE
95
96 parent.__getattr__ = __getattr__
97 return parent