Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

166 lines
6.3 KiB
Python
Raw Normal View History

# SPDX-FileCopyrightText: 2023 Blender Authors
PyAPI: add-on name-spacing for extension repositories Support name-spaced add-ons, exposed via user configurable extension repositories. Directories for add-ons can be added at run-time and are name-spaced to avoid name-collisions with Python modules or add-ons from other repositories. This is exposed as an experimental feature "Extension Repositories". Details: - A `bUserExtensionRepo` type which represents a repository which is listed in the add-ons repository. - `JunctionModuleHandle` class to manage a package with sub-modules which can point to arbitrary locations. - `bpy.app.handlers._extension_repos_update_{pre/post}` internal callbacks run before/after changes to extension repositories, callbacks are used to sync the changes to the Python package that exposes these to add-ons. - The size of an add-on name has been increased so a user-defined package prefix can be included without enforcing shorter add-on names. - Functionality relating to package management has been left out of this change and will be developed separately. Further work: - While a repository can be renamed, enabled add-ons aren't renamed. Eventually we might want to support this although we could also disallow renaming repositories with add-ons enabled as the name isn't all that significant. - Removing a repository should remove all the add-ons located in this repository. - Sub-module names are currently restricted to `[A-Za-z]+[A-Za-z0-9_]*` we might want to relax this to allow unicode characters (we might still want to disallow `-` or any characters that would prevent attribute access in code). Ref !110869. Reviewed By: brecht
2023-08-09 20:15:34 +10:00
#
# 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
PyAPI: add-on name-spacing for extension repositories Support name-spaced add-ons, exposed via user configurable extension repositories. Directories for add-ons can be added at run-time and are name-spaced to avoid name-collisions with Python modules or add-ons from other repositories. This is exposed as an experimental feature "Extension Repositories". Details: - A `bUserExtensionRepo` type which represents a repository which is listed in the add-ons repository. - `JunctionModuleHandle` class to manage a package with sub-modules which can point to arbitrary locations. - `bpy.app.handlers._extension_repos_update_{pre/post}` internal callbacks run before/after changes to extension repositories, callbacks are used to sync the changes to the Python package that exposes these to add-ons. - The size of an add-on name has been increased so a user-defined package prefix can be included without enforcing shorter add-on names. - Functionality relating to package management has been left out of this change and will be developed separately. Further work: - While a repository can be renamed, enabled add-ons aren't renamed. Eventually we might want to support this although we could also disallow renaming repositories with add-ons enabled as the name isn't all that significant. - Removing a repository should remove all the add-ons located in this repository. - Sub-module names are currently restricted to `[A-Za-z]+[A-Za-z0-9_]*` we might want to relax this to allow unicode characters (we might still want to disallow `-` or any characters that would prevent attribute access in code). Ref !110869. Reviewed By: brecht
2023-08-09 20:15:34 +10:00
from types import ModuleType
from collections.abc import (
PyAPI: add-on name-spacing for extension repositories Support name-spaced add-ons, exposed via user configurable extension repositories. Directories for add-ons can be added at run-time and are name-spaced to avoid name-collisions with Python modules or add-ons from other repositories. This is exposed as an experimental feature "Extension Repositories". Details: - A `bUserExtensionRepo` type which represents a repository which is listed in the add-ons repository. - `JunctionModuleHandle` class to manage a package with sub-modules which can point to arbitrary locations. - `bpy.app.handlers._extension_repos_update_{pre/post}` internal callbacks run before/after changes to extension repositories, callbacks are used to sync the changes to the Python package that exposes these to add-ons. - The size of an add-on name has been increased so a user-defined package prefix can be included without enforcing shorter add-on names. - Functionality relating to package management has been left out of this change and will be developed separately. Further work: - While a repository can be renamed, enabled add-ons aren't renamed. Eventually we might want to support this although we could also disallow renaming repositories with add-ons enabled as the name isn't all that significant. - Removing a repository should remove all the add-ons located in this repository. - Sub-module names are currently restricted to `[A-Za-z]+[A-Za-z0-9_]*` we might want to relax this to allow unicode characters (we might still want to disallow `-` or any characters that would prevent attribute access in code). Ref !110869. Reviewed By: brecht
2023-08-09 20:15:34 +10:00
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,
PyAPI: add-on name-spacing for extension repositories Support name-spaced add-ons, exposed via user configurable extension repositories. Directories for add-ons can be added at run-time and are name-spaced to avoid name-collisions with Python modules or add-ons from other repositories. This is exposed as an experimental feature "Extension Repositories". Details: - A `bUserExtensionRepo` type which represents a repository which is listed in the add-ons repository. - `JunctionModuleHandle` class to manage a package with sub-modules which can point to arbitrary locations. - `bpy.app.handlers._extension_repos_update_{pre/post}` internal callbacks run before/after changes to extension repositories, callbacks are used to sync the changes to the Python package that exposes these to add-ons. - The size of an add-on name has been increased so a user-defined package prefix can be included without enforcing shorter add-on names. - Functionality relating to package management has been left out of this change and will be developed separately. Further work: - While a repository can be renamed, enabled add-ons aren't renamed. Eventually we might want to support this although we could also disallow renaming repositories with add-ons enabled as the name isn't all that significant. - Removing a repository should remove all the add-ons located in this repository. - Sub-module names are currently restricted to `[A-Za-z]+[A-Za-z0-9_]*` we might want to relax this to allow unicode characters (we might still want to disallow `-` or any characters that would prevent attribute access in code). Ref !110869. Reviewed By: brecht
2023-08-09 20:15:34 +10:00
) -> 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] = {}
PyAPI: add-on name-spacing for extension repositories Support name-spaced add-ons, exposed via user configurable extension repositories. Directories for add-ons can be added at run-time and are name-spaced to avoid name-collisions with Python modules or add-ons from other repositories. This is exposed as an experimental feature "Extension Repositories". Details: - A `bUserExtensionRepo` type which represents a repository which is listed in the add-ons repository. - `JunctionModuleHandle` class to manage a package with sub-modules which can point to arbitrary locations. - `bpy.app.handlers._extension_repos_update_{pre/post}` internal callbacks run before/after changes to extension repositories, callbacks are used to sync the changes to the Python package that exposes these to add-ons. - The size of an add-on name has been increased so a user-defined package prefix can be included without enforcing shorter add-on names. - Functionality relating to package management has been left out of this change and will be developed separately. Further work: - While a repository can be renamed, enabled add-ons aren't renamed. Eventually we might want to support this although we could also disallow renaming repositories with add-ons enabled as the name isn't all that significant. - Removing a repository should remove all the add-ons located in this repository. - Sub-module names are currently restricted to `[A-Za-z]+[A-Za-z0-9_]*` we might want to relax this to allow unicode characters (we might still want to disallow `-` or any characters that would prevent attribute access in code). Ref !110869. Reviewed By: brecht
2023-08-09 20:15:34 +10:00
def submodule_items(self) -> Sequence[tuple[str, ModuleType]]:
PyAPI: add-on name-spacing for extension repositories Support name-spaced add-ons, exposed via user configurable extension repositories. Directories for add-ons can be added at run-time and are name-spaced to avoid name-collisions with Python modules or add-ons from other repositories. This is exposed as an experimental feature "Extension Repositories". Details: - A `bUserExtensionRepo` type which represents a repository which is listed in the add-ons repository. - `JunctionModuleHandle` class to manage a package with sub-modules which can point to arbitrary locations. - `bpy.app.handlers._extension_repos_update_{pre/post}` internal callbacks run before/after changes to extension repositories, callbacks are used to sync the changes to the Python package that exposes these to add-ons. - The size of an add-on name has been increased so a user-defined package prefix can be included without enforcing shorter add-on names. - Functionality relating to package management has been left out of this change and will be developed separately. Further work: - While a repository can be renamed, enabled add-ons aren't renamed. Eventually we might want to support this although we could also disallow renaming repositories with add-ons enabled as the name isn't all that significant. - Removing a repository should remove all the add-ons located in this repository. - Sub-module names are currently restricted to `[A-Za-z]+[A-Za-z0-9_]*` we might want to relax this to allow unicode characters (we might still want to disallow `-` or any characters that would prevent attribute access in code). Ref !110869. Reviewed By: brecht
2023-08-09 20:15:34 +10:00
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]
PyAPI: add-on name-spacing for extension repositories Support name-spaced add-ons, exposed via user configurable extension repositories. Directories for add-ons can be added at run-time and are name-spaced to avoid name-collisions with Python modules or add-ons from other repositories. This is exposed as an experimental feature "Extension Repositories". Details: - A `bUserExtensionRepo` type which represents a repository which is listed in the add-ons repository. - `JunctionModuleHandle` class to manage a package with sub-modules which can point to arbitrary locations. - `bpy.app.handlers._extension_repos_update_{pre/post}` internal callbacks run before/after changes to extension repositories, callbacks are used to sync the changes to the Python package that exposes these to add-ons. - The size of an add-on name has been increased so a user-defined package prefix can be included without enforcing shorter add-on names. - Functionality relating to package management has been left out of this change and will be developed separately. Further work: - While a repository can be renamed, enabled add-ons aren't renamed. Eventually we might want to support this although we could also disallow renaming repositories with add-ons enabled as the name isn't all that significant. - Removing a repository should remove all the add-ons located in this repository. - Sub-module names are currently restricted to `[A-Za-z]+[A-Za-z0-9_]*` we might want to relax this to allow unicode characters (we might still want to disallow `-` or any characters that would prevent attribute access in code). Ref !110869. Reviewed By: brecht
2023-08-09 20:15:34 +10:00
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]