aboutsummaryrefslogtreecommitdiffstats
path: root/hypervideo_dl/compat/compat_utils.py
blob: 8e94125d6fe740428e6e469cf695d3aae68df9ec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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):
    return _Package(
        name=getattr(module, '_hypervideo_dl__identifier', module.__name__),
        version=str(next(filter(None, (
            getattr(module, attr, None)
            for attr in ('__version__', 'version_string', 'version')
        )), None)))


def _is_package(module):
    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