diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index ab1c778f6f9..e8dcddfa89d 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -36,8 +36,8 @@ from logHandler import log import UIAUtils from comInterfaces import UIAutomationClient as UIA -# F403 unable to detect undefined names -from comInterfaces.UIAutomationClient import * # noqa: F403 +# F403: unable to detect undefined names +from comInterfaces .UIAutomationClient import * # noqa: F403 import textInfos from typing import Dict from queue import Queue diff --git a/source/comInterfaces/UIAutomationClient.py b/source/comInterfaces/UIAutomationClient.py index a24fbb96e65..2ccf200f4a8 100644 --- a/source/comInterfaces/UIAutomationClient.py +++ b/source/comInterfaces/UIAutomationClient.py @@ -1,3 +1,7 @@ -from comtypes.gen import _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 +try: + from comtypes.gen import _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 +except ModuleNotFoundError: + import _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 + from _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 import * globals().update(_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.__dict__) __name__ = 'comtypes.gen.UIAutomationClient' \ No newline at end of file diff --git a/source/comInterfaces/readme.md b/source/comInterfaces/readme.md new file mode 100644 index 00000000000..b28d495ea72 --- /dev/null +++ b/source/comInterfaces/readme.md @@ -0,0 +1,16 @@ +The comInterfaces package is generated via SCons. +The logic for this is in `comInterfaces_sconscript`, which uses `comtypes.gen` to read `*.tlb` +files or via interface IDs. + +The interface files have an ID named file (a GUID, followed by a version number) as well as a +"friendly name" file. + +The "friendly name" file generated by comtypes is not consumed easily by tools and IDEs, +runtime logic is used to expose symbols. +To remedy this, the file is then processed by `comInterfaces_sconscript` to extract the module +name and replace the import statement with a more elaborate approach which includes a fallback +for the purposes of IDEs and tools. + +Only UIAutomation.py is not generated, UIA has historically been updated regularly and the version on +Appveyor build servers could not be guaranteed. + diff --git a/source/comInterfaces_sconscript b/source/comInterfaces_sconscript index 12e5ec73134..52ff137407d 100755 --- a/source/comInterfaces_sconscript +++ b/source/comInterfaces_sconscript @@ -11,6 +11,10 @@ #This license can be found at: #http://www.gnu.org/licenses/old-licenses/gpl-2.0.html ### +from typing import List +import fileinput +import re +from SCons.Node.FS import File Import( 'env', @@ -26,18 +30,49 @@ def new_my_import(fullname): return old_my_import(fullname) comtypes.client._generate._my_import = new_my_import -def interfaceAction(target,source,env): - clsid=env.get('clsid') +def makeIDEFriendly(path:str) -> None: + """ + Add a local import of * so that tools and IDE's can find definitions. + Prefer to import from comtypes.gen, at runtime behavior will not have changed. + @param path: Path to the friendly name comInterfaces module. + """ + importTemplate = ( +"""try: + from comtypes.gen import {libIdentifier} +except ModuleNotFoundError: + import {libIdentifier} + from {libIdentifier} import * +""") + + importPattern = re.compile(r"from comtypes\.gen import ([\w]+)\n") + for line in fileinput.input(path, inplace=True): + # Note: The fileinput module temporarily redirects stdout to the file. + # So print is used to write the file. + match = importPattern.match(line) + if match: + libId = match.group(1) + print(importTemplate.format(libIdentifier=libId), end='') + else: + print(line, end='') + +def interfaceAction(target:List[File], source, env): + clsid = env.get('clsid') if clsid: - source=(clsid,env['majorVersion'],env['minorVersion']) + source=(clsid, env['majorVersion'], env['minorVersion']) else: source=str(source[0]) comtypes.client.GetModule(source) + # re-write the the "friendlyNameFile" so that tools/IDEs can find the + # definitions + for t in target: + path: str = t.abspath + if path.endswith(".py"): + makeIDEFriendly(path) interfaceBuilder=env.Builder( action=env.Action(interfaceAction), ) -env['BUILDERS']['comtypesInterface']=interfaceBuilder +env['BUILDERS']['comtypesInterface'] = interfaceBuilder # Force comtypes generated interfaces in to our directory import comtypes.client @@ -47,21 +82,40 @@ COM_INTERFACES = { "IAccessible2Lib.py": "typelibs/ia2.tlb", "ISimpleDOM.py": "typelibs/ISimpleDOMNode.tlb", "mathPlayer.py": "typelibs/mathPlayerDLL.tlb", - #"Accessibility.py": ('{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}',1,0), + "Accessibility.py": ('{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}',1,0), "tom.py": ('{8CC497C9-A1DF-11CE-8098-00AA0047BE5D}',1,0), "SpeechLib.py": ('{C866CA3A-32F7-11D2-9602-00C04F8EE628}',5,0), "AcrobatAccessLib.py": "typelibs/AcrobatAccess.tlb", + # We don't generate UIAutomationClient (and _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py) + # because we don't know what version is available on Appveyor. + # Instead we locally generate the file and include it in the repository. + # Ensuring that adjustments performed by 'makeIDEFriendly' are incorporated. + # "UIAutomationClient.py": ('{944DE083-8FB8-45CF-BCB7-C477ACB2F897}', 1, 0), } for k,v in COM_INTERFACES.items(): - targets=[Dir('comInterfaces').File(k), - # This buillds a .pyc file as well. - Dir('comInterfaces').File(importlib.util.cache_from_source(k))] - source=clsid=majorVersion=None + targets=[ + Dir('comInterfaces').File(k), + # This builds a .pyc file as well. + Dir('comInterfaces').File(importlib.util.cache_from_source(k)) + ] + source = clsid = majorVersion = None if isinstance(v, str): - env.comtypesInterface(targets,v) + env.comtypesInterface(targets, v) else: - env.comtypesInterface(targets,Dir('comInterfaces').File('__init__.py'),clsid=v[0],majorVersion=v[1],minorVersion=v[2]) + env.comtypesInterface( + targets, + Dir('comInterfaces').File('__init__.py'), + clsid=v[0], + majorVersion=v[1], + minorVersion=v[2] + ) + -#When cleaning comInterfaces get rid of everything except for things starting with __ (e.g. __init__.py) -env.Clean(Dir('comInterfaces'),Glob('comInterfaces/[!_]*')+Glob('comInterfaces/_[!_]*')) + +# When cleaning comInterfaces get rid of everything +# except for things starting with __ (e.g. __init__.py) +env.Clean( + Dir('comInterfaces'), + Glob('comInterfaces/[!_]*') + Glob('comInterfaces/_[!_]*') +) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index f1d6d595446..8b0f48b88d4 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -91,6 +91,7 @@ What's New in NVDA - `WelcomeDialog`, `LauncherDialog` and `AskAllowUsageStatsDialog` are moved to the `gui.startupDialogs`. (#12105) - `getDocFilePath` has been moved from `gui` to the `documentationUtils` module. (#12105) - The gui.accPropServer module as well as the AccPropertyOverride and ListCtrlAccPropServer classes from the gui.nvdaControls module have been removed in favor of WX' native support for overriding accessibility properties. When enhancing accessibility of WX controls, implement wx.Accessible instead. (#12215) +- Files in `source/comInterfaces/` are now more easily consumable by developer tools such as IDEs. (#12201) = 2020.4 =