11# A part of NonVisual Desktop Access (NVDA)
2- # Copyright (C) 2012-2022 Rui Batista, NV Access Limited, Noelia Ruiz Martínez,
2+ # Copyright (C) 2012-2023 Rui Batista, NV Access Limited, Noelia Ruiz Martínez,
33# Joseph Lee, Babbage B.V., Arnold Loubriat, Łukasz Golonka, Leonard de Ruijter
44# This file is covered by the GNU General Public License.
55# See the file COPYING for more details.
1111import inspect
1212import itertools
1313import collections
14- import pkgutil
1514import shutil
1615from io import StringIO
1716import pickle
2928import addonAPIVersion
3029from . import addonVersionCheck
3130from .addonVersionCheck import isAddonCompatible
31+ from .packaging import isModuleName
32+ import importlib
33+ from types import ModuleType
3234import extensionPoints
3335
3436
@@ -475,25 +477,44 @@ 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
483+ @raises: Any exception that can be raised when importing a module, such as NameError, AttributeError, ImportError, etc.
484+ a ValueError is raised when the module name is invalid.
484485 """
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
486+ if not isModuleName (name ):
487+ raise ValueError (f"{ name } is an invalid python module name" )
488+ log .debug (f"Importing module { name } from plugin { self !r} " )
490489 # 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
496- return None
490+ fullName = f"addons.{ self .name } .{ name } "
491+ # If the given name contains dots (i.e. it is a submodule import),
492+ # ensure the module at the top of the hierarchy is created correctly.
493+ # After that, the import mechanism will be able to resolve the submodule automatically.
494+ splitName = name .split ('.' )
495+ fullNameTop = f"addons.{ self .name } .{ splitName [0 ]} "
496+ if fullNameTop in sys .modules :
497+ # The module can safely be imported, since the top level module is known.
498+ return importlib .import_module (fullName )
499+ # Ensure the new module is resolvable by the import system.
500+ # For this, all packages in the tree have to be available in sys.modules.
501+ # We add mock modules for the addons package and the addon itself.
502+ # If we don't do this, namespace packages can't be imported correctly.
503+ for parentName in ("addons" , f"addons.{ self .name } " ):
504+ if parentName in sys .modules :
505+ # Parent package already initialized
506+ continue
507+ parentSpec = importlib ._bootstrap .ModuleSpec (parentName , None , is_package = True )
508+ parentModule = importlib .util .module_from_spec (parentSpec )
509+ sys .modules [parentModule .__name__ ] = parentModule
510+ spec = importlib .machinery .PathFinder .find_spec (fullNameTop , [self .path ])
511+ if not spec :
512+ raise ModuleNotFoundError (importlib ._bootstrap ._ERR_MSG .format (name ), name = name )
513+ mod = importlib .util .module_from_spec (spec )
514+ sys .modules [fullNameTop ] = mod
515+ if spec .loader :
516+ spec .loader .exec_module (mod )
517+ return mod if fullNameTop == fullName else importlib .import_module (fullName )
497518
498519 def getTranslationsInstance (self , domain = 'nvda' ):
499520 """ Gets the gettext translation instance for this add-on.
@@ -511,7 +532,11 @@ def runInstallTask(self,taskName,*args,**kwargs):
511532 in the add-on's installTasks module if it exists.
512533 """
513534 if not hasattr (self ,'_installTasksModule' ):
514- self ._installTasksModule = self .loadModule ('installTasks' )
535+ try :
536+ installTasksModule = self .loadModule ('installTasks' )
537+ except ModuleNotFoundError :
538+ installTasksModule = None
539+ self ._installTasksModule = installTasksModule
515540 if self ._installTasksModule :
516541 func = getattr (self ._installTasksModule ,taskName ,None )
517542 if func :
0 commit comments