Skip to content

Commit 4adfbfb

Browse files
authored
Merge c3c884a into 4ed4331
2 parents 4ed4331 + c3c884a commit 4adfbfb

4 files changed

Lines changed: 60 additions & 21 deletions

File tree

include/espeak

Submodule espeak updated 350 files

include/nvda-cldr

Submodule nvda-cldr updated 66 files

source/addonHandler/__init__.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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.
@@ -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,6 +28,9 @@
2928
import addonAPIVersion
3029
from . import addonVersionCheck
3130
from .addonVersionCheck import isAddonCompatible
31+
from .packaging import isModuleName
32+
import importlib
33+
from types import ModuleType
3234
import 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:

source/addonHandler/packaging.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2009-2022 NV Access Limited, Rui Batista, Zahari Yurukov, Leonard de Ruijter
2+
# Copyright (C) 2009-2023 NV Access Limited, Rui Batista, Zahari Yurukov, Leonard de Ruijter
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

@@ -11,6 +11,7 @@
1111
from types import ModuleType
1212
import globalVars
1313
import config
14+
from keyword import iskeyword
1415

1516

1617
def initializeModulePackagePaths():
@@ -60,3 +61,16 @@ def addDirsToPythonPackagePath(module: ModuleType, subdir: Optional[str] = None)
6061
pathList = [fullPath]
6162
pathList.extend(module.__path__)
6263
module.__path__ = pathList
64+
65+
66+
def isModuleName(name: str) -> bool:
67+
"""When adding a module to sys.modules, it is important to check module name validity.
68+
the L{str.isidentifier} method checks whether a string is a valid python identifier,
69+
however this includes identifiers like 'def' and 'class', which are definitely invalid module names.
70+
Therefore a valid module name should be an identifier but not a keyword.
71+
A valid module name can also contain dots, but a dot is considered invalid in identifiers.
72+
Therefore, use dot as a split separator and check all the name parts independently.
73+
@param moduleName: De module name to check for naming conventions.
74+
@returns: Whether the module name is valid.
75+
"""
76+
return all(n.isidentifier() and not iskeyword(n) for n in name.split("."))

0 commit comments

Comments
 (0)