The NVDA API must maintain compatibility with add-ons throughout yearly development cycles.
The first release of a year, i.e. 20XX.1, is when the NVDA API can introduce breaking changes.
Where possible, ensure the NVDA API maintains backwards compatibility. If no removal is required or proposed, backwards compatibility can be maintained via the following snippet:
import NVDAState
def __getattr__(attrName: str) -> Any:
"""Module level `__getattr__` used to preserve backward compatibility."""
if attrName == "deprecatedSymbolName" and NVDAState._allowDeprecatedAPI():
# Note: this should only log in situations where it will not be excessively noisy.
log.warning(
"Importing deprecatedSymbolName from here is deprecated. "
"Import X instead and do Y. ",
# Include stack info so testers can report warning to add-on author.
stack_info=True,
)
# Ensure the API of deprecatedSymbolNameReplacement is the same as the deprecated symbol.
return deprecatedSymbolNameReplacement
raise AttributeError(f"module {repr(__name__)} has no attribute {repr(attrName)}")Support for deprecations is included in the various extensionPoint classes.
For example:
filter_something = extensionPoints.Filter[int](
_deprecationMessage="filter_something is deprecated. Use filter_somethingElse instead.",
)The deprecation message is logged at the warning level when calling register on a HandlerRegistrar.
When NVDAState._allowDeprecatedAPI() returns False, a RuntimeError is raised instead.
In order to improve the NVDA API, changes that will break future compatibility may be implemented, as long as they retain backwards compatibility until the 20XX.1 release.
This can be done by using a version check to automate deprecation.
For example, if you wish to replace usages of deprecatedSymbolName with newSymbolName.
When we begin work on NEXT_YEAR, we update BACK_COMPAT_TO, which introduces the add-on API breakage warning.
At this stage, deprecatedSymbolName will no longer be part of the NVDA API and all internal usages must be removed prior.
from addonAPIVersion import BACK_COMPAT_TO
import NVDAState
if BACK_COMPAT_TO < (NEXT_YEAR, 1, 0) and NVDAState._allowDeprecatedAPI():
deprecatedSymbolName = newSymbolNameThese snippets do not support re-assignment to deprecatedSymbolName.
As a result, it not possible to retain backwards compatibility for module level variables which are expected to support assignment.
For example, if there is a global variable named currentState in exampleModule.py, currentState cannot be deprecated.
This is because:
__getattr__only fetches variables which are not defined on the module level. As such, setting an attribute that is fetched via__getattr__will prevent__getattr__from fetching the attribute. Any future gets will fetch the newly assigned value.- Python does not have an equivalent module level
__setattr__like the__getattr__example. - Encapsulating a module level variable into another data structure changes the import behaviour.
- e.g. if
exampleModulebecame a class, soexampleModule.currentStatewas a class level variable with the same namespace as the deprecated symbol,exampleModulemay not be importable in the same manner.
- e.g. if
As a result, module level variables should be avoided.
To ensure a module retains the same symbol names being importable, check across versions what is imported using the NVDA python console.
import controlTypes
dir(controlTypes)Changes different to moving or renaming symbols need to be considered carefully with a different approach.
Any API breaking changes such as deprecations marked for removal should be commented with the year of intended removal, and notes on how to implement the API change as an add-on developer and NVDA developer.
Deprecations should be announced via the NVDA API mailing list.