|
11 | 11 | import inspect |
12 | 12 | import itertools |
13 | 13 | import collections |
14 | | -import pkgutil |
15 | 14 | import shutil |
16 | 15 | from io import StringIO |
17 | 16 | import pickle |
|
29 | 28 | import addonAPIVersion |
30 | 29 | from . import addonVersionCheck |
31 | 30 | from .addonVersionCheck import isAddonCompatible |
| 31 | +import importlib |
| 32 | +from types import ModuleType |
32 | 33 | import extensionPoints |
| 34 | +from keyword import iskeyword |
33 | 35 |
|
34 | 36 |
|
35 | 37 | MANIFEST_FILENAME = "manifest.ini" |
@@ -475,25 +477,41 @@ def _getPathForInclusionInPackage(self, package): |
475 | 477 | extension_path = os.path.join(self.path, package.__name__) |
476 | 478 | return extension_path |
477 | 479 |
|
478 | | - def loadModule(self, name): |
| 480 | + def loadModule(self, name: str) -> ModuleType: |
479 | 481 | """ loads a python module from the addon directory |
480 | 482 | @param name: the module name |
481 | | - @type name: string |
482 | | - @returns the python module with C{name} |
483 | | - @rtype python module |
484 | 483 | """ |
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}") |
490 | 488 | # 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: |
496 | 509 | 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) |
497 | 515 |
|
498 | 516 | def getTranslationsInstance(self, domain='nvda'): |
499 | 517 | """ Gets the gettext translation instance for this add-on. |
|
0 commit comments