From ad0f6e367005d6d7d765bb634cf8bcf35825d112 Mon Sep 17 00:00:00 2001 From: Michael Weghorn Date: Tue, 26 Nov 2024 10:30:26 +0100 Subject: [PATCH 1/9] Support IAccessible2 labelled-by relation ### Link to issue number: Fixes #17436 ### Summary of the issue: IAccessible2 has an IA2_RELATION_LABELLED_BY relation type, see https://accessibility.linuxfoundation.org/a11yspecs/ia2/docs/html/group__grp_relations.html#ga7bbace7ffb57476b75d621af2e27d1ff . However, that one was not taken into account by NVDA's "labeledBy" property for objects. This could could be seen for example with LibreOffice that implements handling of that IAccessible2 relation. Additionally, a `None` return value from IAccessibleHandler.accNavigate wasn't handled in `IAccessible._get_labeledBy`, triggering an error, e.g.: >>> focus.labeledBy Traceback (most recent call last): File "", line 1, in File "C:\tools\cygwin\home\user\development\git\nvda\source\baseObject.py", line 59, in __get__ return instance._getPropertyViaCache(self.fget) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\tools\cygwin\home\user\development\git\nvda\source\baseObject.py", line 167, in _getPropertyViaCache val = getterMethod(self) ^^^^^^^^^^^^^^^^^^ File "C:\tools\cygwin\home\user\development\git\nvda\source\NVDAObjects\IAccessible\__init__.py", line 1167, in _get_labeledBy (pacc, accChild) = IAccessibleHandler.accNavigate( ^^^^^^^^^^^^^^^^ TypeError: cannot unpack non-iterable NoneType object ### Description of user facing changes In NVDA's Python console, retrieving the "labeledBy" property now works for objects in applications implementing the "labelled-by" IAccessible2 relation and no longer triggers an error. ### Description of development approach * Extend `IAccessibleHandler.RelationType` with the `LABELLED_BY` relation as defined in the IAccessible2 spec. * Use this relation in `IAccessible._get_labeledBy` before falling back to trying a custom Mozilla/Gecko specific NAVRELATION. * Add handling for the case where `IAccessibleHandler.accNavigate` for the custom Mozilla/Geck approach returns `None` to fix the error triggered otherwise when no relation is set. ### Testing strategy: 1. start NVDA 2. Start LibreOffice Writer 3. open the options dialog: "Tools" -> "Options", go to the "User Data" page 4. move focus to the "Company" text edit 5. open NVDA's Python console (Ctrl+Insert+Z) 6. print the object that the currently focused edit is labelled by, and it's accessible name and role: >>> focus.labeledBy >>> focus.labeledBy.name 'Company:' >>> focus.labeledBy.role ### Known issues with pull request: None ### Code Review Checklist: - [x] Documentation: - Change log entry - User Documentation - Developer / Technical Documentation - Context sensitive help for GUI changes - [x] Testing: - Unit tests - System (end to end) tests - Manual testing - [x] UX of all users considered: - Speech - Braille - Low Vision - Different web browsers - Localization in other languages / culture than English - [x] API is compatible with existing add-ons. - [x] Security precautions taken. @coderabbitai summary --- source/IAccessibleHandler/types.py | 1 + source/NVDAObjects/IAccessible/__init__.py | 9 ++++++++- user_docs/en/changes.md | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/source/IAccessibleHandler/types.py b/source/IAccessibleHandler/types.py index 2d9e81a39ba..ec4318d7252 100644 --- a/source/IAccessibleHandler/types.py +++ b/source/IAccessibleHandler/types.py @@ -29,3 +29,4 @@ class RelationType(str, enum.Enum): CONTROLLER_FOR = "controllerFor" ERROR = "error" ERROR_FOR = "errorFor" + LABELLED_BY = "labelledBy" diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index 2ab07700488..ce8a10bd0af 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -1163,12 +1163,19 @@ def isPointInObject(self, x, y): return True def _get_labeledBy(self): + label = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.LABELLED_BY) + if label: + return label + try: - (pacc, accChild) = IAccessibleHandler.accNavigate( + ret = IAccessibleHandler.accNavigate( self.IAccessibleObject, self.IAccessibleChildID, IAccessibleHandler.NAVRELATION_LABELLED_BY, ) + if not ret: + return None + (pacc, accChild) = ret obj = IAccessible(IAccessibleObject=pacc, IAccessibleChildID=accChild) return obj except COMError: diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 4fd9af5c4a2..92bb710d28f 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -70,6 +70,7 @@ Specifically, MathML inside of span and other elements that have the attribute ` * Opening the NVDA Python Console will no longer fail in case an error occurs while retrieving snapshot variables. (#17391, @CyrilleB79) * In Notepad and other UIA documents on Windows 11, if the last line is empty, the `braille next line command` will move the cursor to the last line. In any document, if the cursor is on the last line, it will be moved to the end when using this command. (#17251, @nvdaes) +* In NVDA's Python console, retrieving the "labeledBy" property now works for objects in applications implementing the "labelled-by" IAccessible2 relation and no longer triggers an error. (#17436, @michaelweghorn) ### Changes for Developers From feea4aa0aa0a6b12c33ee8dd70f22d76a2cc6d83 Mon Sep 17 00:00:00 2001 From: Michael Weghorn Date: Wed, 27 Nov 2024 09:12:38 +0000 Subject: [PATCH 2/9] Add type annotation --- source/NVDAObjects/IAccessible/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index ce8a10bd0af..a6b50c19d59 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -3,6 +3,7 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. +from __future__ import annotations import typing from typing import ( Generator, @@ -1162,7 +1163,7 @@ def isPointInObject(self, x, y): return False return True - def _get_labeledBy(self): + def _get_labeledBy(self) -> IAccessible | None: label = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.LABELLED_BY) if label: return label From 00b1ca9550d12c56700bf13547d2ec151973160e Mon Sep 17 00:00:00 2001 From: Michael Weghorn Date: Wed, 27 Nov 2024 09:15:01 +0000 Subject: [PATCH 3/9] Move changelog entry to "Changes for Developers" section --- user_docs/en/changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 92bb710d28f..5215c2fc7ad 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -70,7 +70,6 @@ Specifically, MathML inside of span and other elements that have the attribute ` * Opening the NVDA Python Console will no longer fail in case an error occurs while retrieving snapshot variables. (#17391, @CyrilleB79) * In Notepad and other UIA documents on Windows 11, if the last line is empty, the `braille next line command` will move the cursor to the last line. In any document, if the cursor is on the last line, it will be moved to the end when using this command. (#17251, @nvdaes) -* In NVDA's Python console, retrieving the "labeledBy" property now works for objects in applications implementing the "labelled-by" IAccessible2 relation and no longer triggers an error. (#17436, @michaelweghorn) ### Changes for Developers @@ -100,6 +99,7 @@ Add-ons will need to be re-tested and have their manifest updated. * Removed the requirement to indent function parameter lists by two tabs from NVDA's Coding Standards, to be compatible with modern automatic linting. (#17126, @XLTechie) * Added the [VS Code workspace configuration for NVDA](https://nvaccess.org/nvaccess/vscode-nvda) as a git submodule. (#17003) * In the `brailleTables` module, a `getDefaultTableForCurrentLang` function has been added (#17222, @nvdaes) +* Retrieving the "labeledBy" property now works for objects in applications implementing the "labelled-by" IAccessible2 relation and no longer triggers an error. (#17436, @michaelweghorn) #### API Breaking Changes From 93c3ee5bae9e890f53e952d1a91b5ba2fc7d2fed Mon Sep 17 00:00:00 2001 From: Michael Weghorn Date: Wed, 27 Nov 2024 09:28:44 +0000 Subject: [PATCH 4/9] Avoid type clash due to typing.List import Use qualified name. Otherwise the newly introduced from __future__ import annotations in commit feea4aa0aa0a6b12c33ee8dd70f22d76a2cc6d83 Author: Michael Weghorn Date: Wed Nov 27 09:12:38 2024 +0000 Add type annotation causes source/NVDAObjects/IAccessible/__init__.py:2391:7: F811 Redefinition of unused `List` from line 13 | 2391 | class List(IAccessible): | ^^^^ F811 2392 | def _get_role(self): 2393 | return controlTypes.Role.LIST | = help: Remove definition: `List` Found 1 error. --- source/NVDAObjects/IAccessible/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index a6b50c19d59..cb563a62cf6 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -10,7 +10,6 @@ Optional, Tuple, Union, - List, ) from comtypes.automation import IEnumVARIANT, VARIANT @@ -1948,7 +1947,7 @@ def _get_detailsRelations(self) -> Tuple["IAccessible"]: # due to caching of baseObject.AutoPropertyObject, do not attempt to return a generator. return tuple(detailsRelsGen) - def _get_controllerFor(self) -> List[NVDAObject]: + def _get_controllerFor(self) -> typing.List[NVDAObject]: control = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.CONTROLLER_FOR) if control: return [control] From 1c6148de9430c941cd8cb955aba6f9f0d57b95c6 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Fri, 29 Nov 2024 15:21:46 +1100 Subject: [PATCH 5/9] Apply suggestions from code review --- source/NVDAObjects/IAccessible/__init__.py | 2 +- user_docs/en/changes.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index cb563a62cf6..d2af95654d2 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -1947,7 +1947,7 @@ def _get_detailsRelations(self) -> Tuple["IAccessible"]: # due to caching of baseObject.AutoPropertyObject, do not attempt to return a generator. return tuple(detailsRelsGen) - def _get_controllerFor(self) -> typing.List[NVDAObject]: + def _get_controllerFor(self) -> list[NVDAObject]: control = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.CONTROLLER_FOR) if control: return [control] diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 5215c2fc7ad..e4b34e62b10 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -99,7 +99,7 @@ Add-ons will need to be re-tested and have their manifest updated. * Removed the requirement to indent function parameter lists by two tabs from NVDA's Coding Standards, to be compatible with modern automatic linting. (#17126, @XLTechie) * Added the [VS Code workspace configuration for NVDA](https://nvaccess.org/nvaccess/vscode-nvda) as a git submodule. (#17003) * In the `brailleTables` module, a `getDefaultTableForCurrentLang` function has been added (#17222, @nvdaes) -* Retrieving the "labeledBy" property now works for objects in applications implementing the "labelled-by" IAccessible2 relation and no longer triggers an error. (#17436, @michaelweghorn) +* Retrieving the `labeledBy` property now works for objects in applications implementing the `labelled-by` IAccessible2 relation and no longer triggers an error. (#17436, @michaelweghorn) #### API Breaking Changes From 6dbe10f871a8daaa8ee071938173dd9fe7b9c61d Mon Sep 17 00:00:00 2001 From: Michael Weghorn Date: Fri, 29 Nov 2024 08:29:37 +0000 Subject: [PATCH 6/9] Add comment why import is needed Without deferred evaluation (s. PEPs mentioned in the newly added comment): $ ./runlint.bat Ensuring NVDA Python virtual environment call ruff check --fix source\NVDAObjects\IAccessible\__init__.py:1165:30: F821 Undefined name `IAccessible` | 1163 | return True 1164 | 1165 | def _get_labeledBy(self) -> IAccessible | None: | ^^^^^^^^^^^ F821 1166 | label = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.LABELLED_BY) 1167 | if label: | Found 1 error. Deactivating NVDA Python virtual environment --- source/NVDAObjects/IAccessible/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index d2af95654d2..6576d9909a3 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -3,7 +3,11 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. +# required for Python < 3.14 for IAccessible return type annotation +# within IAccessible class itself, see https://peps.python.org/pep-0563/ +# and and https://peps.python.org/pep-0649/ from __future__ import annotations + import typing from typing import ( Generator, From 5210d6a958919cbfd01e17e41c03511f88859b80 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 2 Dec 2024 10:02:07 +1100 Subject: [PATCH 7/9] Apply suggestions from code review --- source/NVDAObjects/IAccessible/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index 6576d9909a3..ac7e8faa63c 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -3,10 +3,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# required for Python < 3.14 for IAccessible return type annotation -# within IAccessible class itself, see https://peps.python.org/pep-0563/ -# and and https://peps.python.org/pep-0649/ -from __future__ import annotations import typing from typing import ( @@ -1166,7 +1162,7 @@ def isPointInObject(self, x, y): return False return True - def _get_labeledBy(self) -> IAccessible | None: + def _get_labeledBy(self) -> "IAccessible" | None: label = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.LABELLED_BY) if label: return label From 6e4ce4b7f73cfc4d2e82244e775fabd38c2fd064 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 2 Dec 2024 10:02:50 +1100 Subject: [PATCH 8/9] Update source/NVDAObjects/IAccessible/__init__.py --- source/NVDAObjects/IAccessible/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index ac7e8faa63c..76442ac5d8a 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -3,7 +3,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. - import typing from typing import ( Generator, From 26f07556ab3a618ebcef19b39d7b5b478bac67af Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 2 Dec 2024 11:59:47 +1100 Subject: [PATCH 9/9] Update source/NVDAObjects/IAccessible/__init__.py --- source/NVDAObjects/IAccessible/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index 76442ac5d8a..3e2cb96094e 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -1161,7 +1161,7 @@ def isPointInObject(self, x, y): return False return True - def _get_labeledBy(self) -> "IAccessible" | None: + def _get_labeledBy(self) -> "IAccessible | None": label = self._getIA2RelationFirstTarget(IAccessibleHandler.RelationType.LABELLED_BY) if label: return label