diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index 3cebe0f13d2..575df788825 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -827,6 +827,7 @@ The following plugins and drivers can be included in an add-on: - Braille display drivers: Place them in a brailleDisplayDrivers directory in the archive. - Global plugins: Place them in a globalPlugins directory in the archive. - Synthesizer drivers: Place them in a synthDrivers directory in the archive. +- Vision enhancement providers: Place them in a visionEnhancementProviders directory in the archive. - ++ Optional install / Uninstall code ++ @@ -845,6 +846,20 @@ NVDA will look for and execute an onUninstall function in installTasks.py when N After this function completes, the add-on's directory will automatically be removed. As this happens on NVDA startup before other components are initialized, this function cannot request input from the user. +++ Optional plugin independent code ++ +In NVDA 2023.1 and above, it is possible to bundle plugin independent code with an add-on. +This can be used to bundle base classes, constants, etc. that have to be available to multiple plugins in the add-on. +This is especially useful if your add-on bundles an appModule and a globalPlugin and both plugins need to access the same class bundled with the add-on. + +Plugin independent code can be accessed from the addons namespace, for example under addons.myTestAddon. +If your add-on has a folder called ``lib`` in its root directory that contains a python module called ``constants.py``, the constants module can be imported and used as follows: +``` +from addons.myTestAddon.lib import constants + +if constants.SOME_BOOLEAN_CONSTANT is True: + # perform action +``` + ++ Localizing Add-ons ++ It is possible to provide locale-specific information and messages for your add-on. Locale information can be stored in a locale directory in the archive. diff --git a/source/addonHandler/__init__.py b/source/addonHandler/__init__.py index b01a6adcc41..7dee51a93eb 100644 --- a/source/addonHandler/__init__.py +++ b/source/addonHandler/__init__.py @@ -1,6 +1,6 @@ # A part of NonVisual Desktop Access (NVDA) # Copyright (C) 2012-2022 Rui Batista, NV Access Limited, Noelia Ruiz Martínez, -# Joseph Lee, Babbage B.V., Arnold Loubriat, Łukasz Golonka +# Joseph Lee, Babbage B.V., Arnold Loubriat, Łukasz Golonka, Leonard de Ruijter # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -21,7 +21,7 @@ import zipfile from configobj import ConfigObj from configobj.validate import Validator -from .packaging import initializeModulePackagePaths +from .packaging import initializeAddonsNamespacePackage, initializeModulePackagePaths import config import languageHandler from logHandler import log @@ -30,6 +30,7 @@ from . import addonVersionCheck from .addonVersionCheck import isAddonCompatible import extensionPoints +from types import ModuleType MANIFEST_FILENAME = "manifest.ini" @@ -168,6 +169,7 @@ def initialize(): getAvailableAddons(refresh=True, isFirstLoad=True) state.cleanupRemovedDisabledAddons() state.save() + initializeAddonsNamespacePackage() initializeModulePackagePaths() @@ -395,7 +397,7 @@ def completeRemove(self,runUninstallTask=True): _blockedAddons.discard(self.name) state.save() - def addToPackagePath(self, package): + def addToPackagePath(self, package: ModuleType): """ Adds this L{Addon} extensions to the specific package path if those exist. This allows the addon to "run" / be available because the package is able to search its path, looking for particular modules. This is used by the following: @@ -403,8 +405,8 @@ def addToPackagePath(self, package): - `appModules` - `synthDrivers` - `brailleDisplayDrivers` + - `visionEnhancementProviders` @param package: the python module representing the package. - @type package: python module. """ # #3090: Ensure that we don't add disabled / blocked add-ons to package path. # By returning here the addon does not "run"/ become active / registered. diff --git a/source/addonHandler/packaging.py b/source/addonHandler/packaging.py index 599108bf7e5..f1af96eac10 100644 --- a/source/addonHandler/packaging.py +++ b/source/addonHandler/packaging.py @@ -7,11 +7,18 @@ """ import os.path -from typing import Optional +from typing import ( + List, + Optional, +) from types import ModuleType import globalVars import config +import sys +import importlib +ADDONS_MODULE_NAME = "addons" +"""The name of an importable module that nessts add-on code for every active add-on.""" def initializeModulePackagePaths(): """Initializes the module package paths for drivers and plugins. @@ -60,3 +67,35 @@ def addDirsToPythonPackagePath(module: ModuleType, subdir: Optional[str] = None) pathList = [fullPath] pathList.extend(module.__path__) module.__path__ = pathList + + +def _createModule(moduleName: str, submoduleSearchLocations: Optional[List[str]] = None): + """Creates a module with the given moduleName and adds it to sys.modules. + This ensures that the module can be imported. + @param moduleName: The name of the module, e.g. addons or addons.example. + @param submoduleSearchLocations: Can be provided if the module has to be a python namespace package. + """ + if moduleName in sys.modules: + # module already initialized + return + spec = importlib._bootstrap.ModuleSpec(moduleName, None) + if submoduleSearchLocations: + spec.submodule_search_locations = submoduleSearchLocations + module = importlib.util.module_from_spec(spec) + sys.modules[module.__name__] = module + + +def initializeAddonsNamespacePackage(): + """Initializes the addons namespace package. + This ensures that all python code in an add-on can be imported, even if code lives + outside one of the standard package paths, such as appModules or globalPlugins. + For example, if an add-on named example has a python module called lib, + that module can be imported with `from addons.example import lib` + """ + # First, ensure that there is a placeholder addons module that does nothing, + # i.e. it has no associated path but is solely there to nest add-ons under it. + _createModule(ADDONS_MODULE_NAME) + + from . import getRunningAddons + for addon in getRunningAddons(): + _createModule(f"{ADDONS_MODULE_NAME}.{addon.name}", [addon.path])