Skip to content

Commit 2b2f3ab

Browse files
authored
Merge 2fc8d56 into f6977fe
2 parents f6977fe + 2fc8d56 commit 2b2f3ab

1 file changed

Lines changed: 33 additions & 15 deletions

File tree

source/addonHandler/__init__.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import inspect
1212
import itertools
1313
import collections
14-
import pkgutil
1514
import shutil
1615
from io import StringIO
1716
import pickle
@@ -29,7 +28,10 @@
2928
import addonAPIVersion
3029
from . import addonVersionCheck
3130
from .addonVersionCheck import isAddonCompatible
31+
import importlib
32+
from types import ModuleType
3233
import extensionPoints
34+
from keyword import iskeyword
3335

3436

3537
MANIFEST_FILENAME = "manifest.ini"
@@ -475,25 +477,41 @@ def _getPathForInclusionInPackage(self, package):
475477
extension_path = os.path.join(self.path, package.__name__)
476478
return extension_path
477479

478-
def loadModule(self, name):
480+
def loadModule(self, name: str) -> ModuleType:
479481
""" loads a python module from the addon directory
480482
@param name: the module name
481-
@type name: string
482-
@returns the python module with C{name}
483-
@rtype python module
484483
"""
485-
log.debug("Importing module %s from plugin %s", name, self.name)
486-
importer = pkgutil.ImpImporter(self.path)
487-
loader = importer.find_module(name)
488-
if not loader:
489-
return None
484+
splitName = name.split('.')
485+
if any(n for n in splitName if not n.isidentifier() or iskeyword(n)):
486+
raise ValueError(f"{name} is an invalid python module name")
487+
log.debug(f"Importing module {name} from plugin {self!r}")
490488
# Create a qualified full name to avoid modules with the same name on sys.modules.
491-
fullname = "addons.%s.%s" % (self.name, name)
492-
try:
493-
return loader.load_module(fullname)
494-
except ImportError:
495-
# in this case return None, any other error throw to be handled elsewhere
489+
fullName = f"addons.{self.name}.{name}"
490+
# If the given name contains dots (i.e. it is a submodule import), ensure the top module is created correctly.
491+
# After that, the import mechanism will be able to resolve the submodule automatically.
492+
fullNameTop = f"addons.{self.name}.{splitName[0]}"
493+
if fullNameTop in sys.modules:
494+
# The module can safely be imported, since the top level module is known.
495+
return importlib.import_module(fullName)
496+
# Ensure the new module is resolvable by the import system.
497+
# For this, all packages in the tree have to be available in sys.modules.
498+
# We add mock modules for the addons package and the addon itself.
499+
# If we don't do this, namespace packages can't be imported correctly.
500+
for parentName in ("addons", f"addons.{self.name}"):
501+
if parentName in sys.modules:
502+
# Parent package already initialized
503+
continue
504+
parentSpec = importlib._bootstrap.ModuleSpec(parentName, None, is_package=True)
505+
parentModule = importlib.util.module_from_spec(parentSpec)
506+
sys.modules[parentModule.__name__] = parentModule
507+
spec = importlib.machinery.PathFinder.find_spec(fullNameTop, [self.path])
508+
if not spec:
496509
return None
510+
mod = importlib.util.module_from_spec(spec)
511+
sys.modules[fullNameTop] = mod
512+
if spec.loader:
513+
spec.loader.exec_module(mod)
514+
return mod if fullNameTop == fullName else importlib.import_module(fullName)
497515

498516
def getTranslationsInstance(self, domain='nvda'):
499517
""" Gets the gettext translation instance for this add-on.

0 commit comments

Comments
 (0)