Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions devDocs/developerGuide.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -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 ++
Expand All @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions source/addonHandler/__init__.py
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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
Expand All @@ -30,6 +30,7 @@
from . import addonVersionCheck
from .addonVersionCheck import isAddonCompatible
import extensionPoints
from types import ModuleType


MANIFEST_FILENAME = "manifest.ini"
Expand Down Expand Up @@ -168,6 +169,7 @@ def initialize():
getAvailableAddons(refresh=True, isFirstLoad=True)
state.cleanupRemovedDisabledAddons()
state.save()
initializeAddonsNamespacePackage()
initializeModulePackagePaths()


Expand Down Expand Up @@ -395,16 +397,16 @@ 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:
- `globalPlugins`
- `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.
Expand Down
41 changes: 40 additions & 1 deletion source/addonHandler/packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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])