import collections
import contextlib
+import functools
import importlib
import sys
import types
-
_NO_ATTRIBUTE = object()
_Package = collections.namedtuple('Package', ('name', 'version'))
def get_package_info(module):
- parent = module.__name__.split('.')[0]
- parent_module = None
- with contextlib.suppress(ImportError):
- parent_module = importlib.import_module(parent)
-
- for attr in ('__version__', 'version_string', 'version'):
- version = getattr(parent_module, attr, None)
- if version is not None:
- break
- return _Package(getattr(module, '_yt_dlp__identifier', parent), str(version))
+ return _Package(
+ name=getattr(module, '_yt_dlp__identifier', module.__name__),
+ version=str(next(filter(None, (
+ getattr(module, attr, None)
+ for attr in ('_yt_dlp__version', '__version__', 'version_string', 'version')
+ )), None)))
def _is_package(module):
- try:
- module.__getattribute__('__path__')
- except AttributeError:
- return False
- return True
-
-
-def passthrough_module(parent, child, *, callback=lambda _: None):
- parent_module = importlib.import_module(parent)
- child_module = None # Import child module only as needed
-
- class PassthroughModule(types.ModuleType):
- def __getattr__(self, attr):
- if _is_package(parent_module):
- with contextlib.suppress(ImportError):
- return importlib.import_module(f'.{attr}', parent)
-
- nonlocal child_module
- child_module = child_module or importlib.import_module(child, 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
+ return '__path__' in vars(module)
+
+
+def _is_dunder(name):
+ return name.startswith('__') and name.endswith('__')
+
+
+class EnhancedModule(types.ModuleType):
+ def __bool__(self):
+ return vars(self).get('__bool__', lambda: True)()
+
+ def __getattribute__(self, attr):
+ try:
+ ret = super().__getattribute__(attr)
+ except AttributeError:
+ if _is_dunder(attr):
+ raise
+ getter = getattr(self, '__getattr__', None)
+ if not getter:
+ raise
+ ret = getter(attr)
+ return ret.fget() if isinstance(ret, property) else ret
+
+
+def passthrough_module(parent, child, allowed_attributes=(..., ), *, callback=lambda _: None):
+ """Passthrough parent module into a child module, creating the parent if necessary"""
+ def __getattr__(attr):
+ if _is_package(parent):
+ with contextlib.suppress(ModuleNotFoundError):
+ return importlib.import_module(f'.{attr}', parent.__name__)
+
+ ret = from_child(attr)
+ if ret is _NO_ATTRIBUTE:
+ raise AttributeError(f'module {parent.__name__} has no attribute {attr}')
+ callback(attr)
+ return ret
+
+ @functools.lru_cache(maxsize=None)
+ def from_child(attr):
+ nonlocal child
+ if attr not in allowed_attributes:
+ if ... not in allowed_attributes or _is_dunder(attr):
+ return _NO_ATTRIBUTE
+
+ if isinstance(child, str):
+ child = importlib.import_module(child, parent.__name__)
+
+ if _is_package(child):
+ with contextlib.suppress(ImportError):
+ return passthrough_module(f'{parent.__name__}.{attr}',
+ importlib.import_module(f'.{attr}', child.__name__))
+
+ with contextlib.suppress(AttributeError):
+ return getattr(child, attr)
+
+ return _NO_ATTRIBUTE
+
+ parent = sys.modules.get(parent, types.ModuleType(parent))
+ parent.__class__ = EnhancedModule
+ parent.__getattr__ = __getattr__
+ return parent