File: junction_module.py

package info (click to toggle)
blender 4.3.2%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 309,564 kB
  • sloc: cpp: 2,385,210; python: 330,236; ansic: 280,972; xml: 2,446; sh: 972; javascript: 317; makefile: 170
file content (165 lines) | stat: -rw-r--r-- 6,447 bytes parent folder | download
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later

"""
JunctionModuleHandle creates a module whose sub-modules are not located
in the same directory on the file-system as usual. Instead the sub-modules are
added into the package from different locations on the file-system.

The ``JunctionModuleHandle`` class is used to manipulate sub-modules at run-time.
This is needed to implement package management functionality, repositories can be added/removed at run-time.
"""

__all__ = (
    "JunctionModuleHandle",
)

import sys

from types import ModuleType
from collections.abc import (
    Sequence,
)


def _module_file_set(module: ModuleType, name_full: str) -> None:
    # File is just an identifier, as this doesn't reference an actual file,
    # it just needs to be descriptive.
    module.__name__ = name_full
    module.__package__ = name_full
    module.__file__ = "[{:s}]".format(name_full)


def _module_create(
        name: str,
        *,
        parent: ModuleType | None = None,
        doc: str | None = None,
) -> ModuleType:
    if parent is not None:
        name_full = parent.__name__ + "." + name
    else:
        name_full = name

    module = ModuleType(name, doc)
    _module_file_set(module, name_full)
    if parent is not None:
        setattr(parent, name, module)
    return module


class JunctionModuleHandle:
    __slots__ = (
        "_module_name",
        "_module",
        "_submodules",
    )

    def __init__(self, module_name: str):
        self._module_name: str = module_name
        self._module: ModuleType | None = None
        self._submodules: dict[str, ModuleType] = {}

    def submodule_items(self) -> Sequence[tuple[str, ModuleType]]:
        return tuple(self._submodules.items())

    def register_module(self) -> ModuleType:
        """
        Register the base module in ``sys.modules``.
        """
        if self._module is not None:
            raise Exception("Module {!r} already registered!".format(self._module))
        if self._module_name in sys.modules:
            raise Exception("Module {:s} already in 'sys.modules'!".format(self._module_name))

        module = _module_create(self._module_name)
        sys.modules[self._module_name] = module

        # Differentiate this, and allow access to the factory (may be useful).
        # `module.__module_factory__ = self`

        self._module = module
        return module

    def unregister_module(self) -> None:
        """
        Unregister the base module in ``sys.modules``.
        Keep everything except the modules name (allowing re-registration).
        """
        # Cleanup `sys.modules`.
        sys.modules.pop(self._module_name, None)
        for submodule_name in self._submodules.keys():
            sys.modules.pop("{:s}.{:s}".format(self._module_name, submodule_name), None)

        # Remove from self.
        self._submodules.clear()
        self._module = None

    def register_submodule(self, submodule_name: str, dirpath: str) -> ModuleType:
        name_full = self._module_name + "." + submodule_name
        if self._module is None:
            raise Exception("Module not registered, cannot register a submodule!")
        if submodule_name in self._submodules:
            raise Exception("Module \"{:s}\" already registered!".format(submodule_name))
        # Register.
        submodule = _module_create(submodule_name, parent=self._module)
        sys.modules[name_full] = submodule

        submodule.__path__ = [dirpath]
        setattr(self._module, submodule_name, submodule)
        self._submodules[submodule_name] = submodule
        return submodule

    def unregister_submodule(self, submodule_name: str) -> None:
        name_full = self._module_name + "." + submodule_name
        if self._module is None:
            raise Exception("Module not registered, cannot register a submodule!")
        # Unregister.
        submodule = self._submodules.pop(submodule_name, None)
        if submodule is None:
            raise Exception("Module \"{:s}\" not registered!".format(submodule_name))
        delattr(self._module, submodule_name)
        del sys.modules[name_full]

        # Remove all sub-modules, to prevent them being reused in the future.
        #
        # While it might not seem like a problem to keep these around it means if a module
        # with the same name is registered later, importing sub-modules uses the cached values
        # from `sys.modules` and does *not* assign the module to the name-space of the new `submodule`.
        # This isn't exactly a bug, it's often assumed that inspecting a module
        # is a way to find its sub-modules, using `dir(submodule)` for example.
        # For more technical example `sys.modules["foo.bar"] == sys.modules["foo"].bar`
        # which can fail with and attribute error unless the modules are cleared here.
        #
        # An alternative solution could be re-attach sub-modules to the modules name-space when its re-registered.
        # This has some advantages since the module doesn't have to be re-imported however it has the down
        # side that stale data would be kept in `sys.modules` unnecessarily in many cases.
        name_full_prefix = name_full + "."
        submodule_name_list = [
            submodule_name for submodule_name in sys.modules.keys()
            if submodule_name.startswith(name_full_prefix)
        ]
        for submodule_name in submodule_name_list:
            del sys.modules[submodule_name]

    def rename_submodule(self, submodule_name_src: str, submodule_name_dst: str) -> None:
        name_full_prev = self._module_name + "." + submodule_name_src
        name_full_next = self._module_name + "." + submodule_name_dst

        submodule = self._submodules.pop(submodule_name_src)
        self._submodules[submodule_name_dst] = submodule

        delattr(self._module, submodule_name_src)
        setattr(self._module, submodule_name_dst, submodule)

        _module_file_set(submodule, name_full_next)

        del sys.modules[name_full_prev]
        sys.modules[name_full_next] = submodule

    def rename_directory(self, submodule_name: str, dirpath: str) -> None:
        # TODO: how to deal with existing loaded modules?
        # In practice this is mostly users setting up directories for the first time.
        submodule = self._submodules[submodule_name]
        submodule.__path__ = [dirpath]