From 8ce04cbbd33ec7258a9f53b1f6cd5628c6b12d98 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 28 Sep 2023 12:11:08 +1000 Subject: [PATCH 1/2] Py 3.11 cleanup --- source/_addonStore/dataManager.py | 4 -- source/_addonStore/install.py | 4 -- source/_addonStore/models/addon.py | 4 -- source/_addonStore/network.py | 4 -- source/addonHandler/__init__.py | 22 ++++--- .../_addonStoreGui/viewModels/addonList.py | 3 - source/gui/_addonStoreGui/viewModels/store.py | 4 -- source/languageHandler.py | 53 +--------------- source/monkeyPatches/__init__.py | 4 -- source/monkeyPatches/enumPatches.py | 60 ------------------- source/utils/displayString.py | 2 +- .../NVDAHighlighter.py | 9 +-- source/winAPI/dpiAwareness.py | 13 ---- source/winUser.py | 4 +- tests/unit/test_controlTypes.py | 12 +--- 15 files changed, 21 insertions(+), 181 deletions(-) delete mode 100644 source/monkeyPatches/enumPatches.py diff --git a/source/_addonStore/dataManager.py b/source/_addonStore/dataManager.py index 67870f83eef..5a218e843fa 100644 --- a/source/_addonStore/dataManager.py +++ b/source/_addonStore/dataManager.py @@ -3,10 +3,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Needed for type hinting CaseInsensitiveDict -# Can be removed in a future version of python (3.8+) -from __future__ import annotations - from copy import deepcopy import json import os diff --git a/source/_addonStore/install.py b/source/_addonStore/install.py index a45844b729e..0fdb98c35fa 100644 --- a/source/_addonStore/install.py +++ b/source/_addonStore/install.py @@ -3,10 +3,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Needed for type hinting CaseInsensitiveDict -# Can be removed in a future version of python (3.8+) -from __future__ import annotations - from os import ( PathLike, ) diff --git a/source/_addonStore/models/addon.py b/source/_addonStore/models/addon.py index 8bf158e0051..cc063130a14 100644 --- a/source/_addonStore/models/addon.py +++ b/source/_addonStore/models/addon.py @@ -3,10 +3,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Needed for type hinting CaseInsensitiveDict -# Can be removed in a future version of python (3.8+) -from __future__ import annotations - import dataclasses import json import os diff --git a/source/_addonStore/network.py b/source/_addonStore/network.py index ba766a01f3f..426a96079e0 100644 --- a/source/_addonStore/network.py +++ b/source/_addonStore/network.py @@ -3,10 +3,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Needed for type hinting Future -# Can be removed in a future version of python (3.8+) -from __future__ import annotations - from concurrent.futures import ( Future, ThreadPoolExecutor, diff --git a/source/addonHandler/__init__.py b/source/addonHandler/__init__.py index e845825ce9b..4afa4dc8a65 100644 --- a/source/addonHandler/__init__.py +++ b/source/addonHandler/__init__.py @@ -4,10 +4,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Needed for type hinting CaseInsensitiveDict, UserDict -# Can be removed in a future version of python (3.8+) -from __future__ import annotations - from abc import abstractmethod, ABC import sys import os.path @@ -79,11 +75,12 @@ isCLIParamKnown = extensionPoints.AccumulatingDecider(defaultDecision=False) -class AddonsState(collections.UserDict): +AddonStateDictT = Dict[AddonStateCategory, CaseInsensitiveSet[str]] + + +class AddonsState(collections.UserDict[AddonStateCategory, CaseInsensitiveSet[str]]): """ Subclasses `collections.UserDict` to preserve backwards compatibility. - In future versions of python (3.8+) UserDict[AddonStateCategory, CaseInsensitiveSet[str]] - can have type information added. AddonStateCategory string enums mapped to a set of the add-on "name/id" currently in that state. Add-ons that have the same ID except differ in casing cause a path collision, as add-on IDs are installed to a case insensitive path. @@ -91,12 +88,12 @@ class AddonsState(collections.UserDict): """ @staticmethod - def _generateDefaultStateContent() -> Dict[AddonStateCategory, CaseInsensitiveSet[str]]: + def _generateDefaultStateContent() -> AddonStateDictT: return { category: CaseInsensitiveSet() for category in AddonStateCategory } - data: Dict[AddonStateCategory, CaseInsensitiveSet[str]] + data: AddonStateDictT manualOverridesAPIVersion: MajorMinorPatch @property @@ -224,7 +221,7 @@ def _cleanupCompatibleAddonsFromDowngrade(self) -> None: self[AddonStateCategory.OVERRIDE_COMPATIBILITY].discard(blockedAddon) -state: AddonsState[AddonStateCategory, CaseInsensitiveSet[str]] = AddonsState() +state = AddonsState() def getRunningAddons() -> "AddonHandlerModelGeneratorT": @@ -762,8 +759,9 @@ def initTranslation(): try: callerFrame = inspect.currentframe().f_back callerFrame.f_globals['_'] = translations.gettext - # Install our pgettext function. - callerFrame.f_globals['pgettext'] = languageHandler.makePgettext(translations) + # Install pgettext and npgettext function. + callerFrame.f_globals['pgettext'] = translations.pgettext + callerFrame.f_globals['npgettext'] = translations.npgettext finally: del callerFrame # Avoid reference problems with frames (per python docs) diff --git a/source/gui/_addonStoreGui/viewModels/addonList.py b/source/gui/_addonStoreGui/viewModels/addonList.py index d0131325943..838b3c573e3 100644 --- a/source/gui/_addonStoreGui/viewModels/addonList.py +++ b/source/gui/_addonStoreGui/viewModels/addonList.py @@ -3,9 +3,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Needed for type hinting CaseInsensitiveDict -# Can be removed in a future version of python (3.8+) -from __future__ import annotations from dataclasses import dataclass from enum import Enum diff --git a/source/gui/_addonStoreGui/viewModels/store.py b/source/gui/_addonStoreGui/viewModels/store.py index 3b4bd821d76..4d42bba91bb 100644 --- a/source/gui/_addonStoreGui/viewModels/store.py +++ b/source/gui/_addonStoreGui/viewModels/store.py @@ -3,10 +3,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Needed for type hinting CaseInsensitiveDict -# Can be removed in a future version of python (3.8+) -from __future__ import annotations - from os import ( PathLike, startfile, diff --git a/source/languageHandler.py b/source/languageHandler.py index f125fd78d30..b24ecb1e5f1 100644 --- a/source/languageHandler.py +++ b/source/languageHandler.py @@ -8,7 +8,6 @@ such as converting Windows locale ID's to friendly names and presenting available languages. """ -import builtins import os import sys import ctypes @@ -27,7 +26,6 @@ Optional, Tuple, Union, - Callable, ) #a few Windows locale constants @@ -290,52 +288,6 @@ def getAvailableLanguages(presentational: bool = False) -> List[Tuple[str, str]] return langs -def makePgettext(translations): - """Obtain a pgettext function for use with a gettext translations instance. - pgettext is used to support message contexts, - but Python 3.7's gettext module doesn't support this, - so NVDA must provide its own implementation. - """ - if isinstance(translations, gettext.GNUTranslations): - def pgettext(context, message): - try: - # Look up the message with its context. - return translations._catalog[u"%s\x04%s" % (context, message)] - except KeyError: - return message - elif isinstance(translations, gettext.NullTranslations): - # A language without a translation catalog, such as English. - def pgettext(context, message): - return message - else: - raise ValueError("%s is Not a GNUTranslations or NullTranslations object" % translations) - return pgettext - - -def makeNpgettext( - translations: Union[None, gettext.GNUTranslations, gettext.NullTranslations], -) -> Callable[[str, str, str, Union[int, float]], str]: - """Obtain a npgettext function for use with a gettext translations instance. - npgettext is used to support message contexts with respect to ngettext, - but Python 3.7's gettext module doesn't support this, - so NVDA must provide its own implementation. - """ - if isinstance(translations, gettext.GNUTranslations): - def npgettext(context: str, msgSingular: str, msgPlural: str, n: Union[int, float]) -> str: - try: - # Look up the message with its context. - return translations._catalog[(f"{context}\x04{msgSingular}", translations.plural(n))] - except KeyError: - return msgSingular if n == 1 else msgPlural - elif isinstance(translations, gettext.NullTranslations): - # A language without a translation catalog, such as English. - def npgettext(context: str, msgSingular: str, msgPlural: str, n: Union[int, float]) -> str: - return msgSingular if n == 1 else msgPlural - else: - raise ValueError("%s is Not a GNUTranslations or NullTranslations object" % translations) - return npgettext - - def getLanguageCliArgs() -> Tuple[str, ...]: """Returns all command line arguments which were used to set current NVDA language or an empty tuple if language has not been specified from the CLI.""" @@ -408,11 +360,8 @@ def setLanguage(lang: str) -> None: if trans is None: trans = _createGettextTranslation("en") - trans.install(names=['ngettext']) + trans.install(names=["pgettext", "npgettext", "ngettext"]) setLocale(getLanguage()) - # Install our pgettext and npgettext functions. - builtins.pgettext = makePgettext(trans) - builtins.npgettext = makeNpgettext(trans) global installedTranslation installedTranslation = weakref.ref(trans) diff --git a/source/monkeyPatches/__init__.py b/source/monkeyPatches/__init__.py index 0f25c62db54..3423218b97f 100644 --- a/source/monkeyPatches/__init__.py +++ b/source/monkeyPatches/__init__.py @@ -13,7 +13,3 @@ def applyMonkeyPatches(): # Apply several monkey patches to comtypes from . import comtypesMonkeyPatches comtypesMonkeyPatches.applyMonkeyPatches() - - # Apply patches to Enum, prevent cyclic references on ValueError during construction - from . import enumPatches - enumPatches.replace__new__() diff --git a/source/monkeyPatches/enumPatches.py b/source/monkeyPatches/enumPatches.py deleted file mode 100644 index 1f6b1d9dc29..00000000000 --- a/source/monkeyPatches/enumPatches.py +++ /dev/null @@ -1,60 +0,0 @@ -# A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2021 NV Access Limited -# This file is covered by the GNU General Public License. -# See the file COPYING for more details. - - -def replace__new__(): - import enum - # prevent cyclic references on ValueError during construction - enum.Enum.__new__ = _replacement__new__ - - -def _replacement__new__(cls, value): - """ Copied from python standard library enum.py class Enum. - Prevent cyclic references on ValueError during construction. - Local variable exc must be deleted, otherwise: - - ref to exc held by the frame - - ref to traceback held by exc - - ref to frame held by traceback - """ - # all enum instances are actually created during class construction - # without calling this method; this method is called by the metaclass' - # __call__ (i.e. Color(3) ), and by pickle - if type(value) is cls: - # For lookups like Color(Color.RED) - return value - # by-value search for a matching enum member - # see if it's in the reverse mapping (for hashable values) - try: - return cls._value2member_map_[value] - except KeyError: - # Not found, no need to do long O(n) search - pass - except TypeError: - # not there, now do long search -- O(n) behavior - for member in cls._member_map_.values(): - if member._value_ == value: - return member - # still not found -- try _missing_ hook - try: - result = cls._missing_(value) - except Exception as e: - e.__context__ = ValueError("%r is not a valid %s" % (value, cls.__name__)) - raise e - - if isinstance(result, cls): - return result - - ve_exc = ValueError( - "%r is not a valid %s" % (value, cls.__name__) - ) - if result is None: - raise ve_exc - - te_exc = TypeError( - 'error in %s._missing_: returned %r instead of None or a valid member' - % (cls.__name__, result) - ) - te_exc.__context__ = ve_exc - raise te_exc diff --git a/source/utils/displayString.py b/source/utils/displayString.py index 652b3c10839..3109a8fb087 100644 --- a/source/utils/displayString.py +++ b/source/utils/displayString.py @@ -34,7 +34,7 @@ class _DisplayStringEnumMixin(ABC): This mixin can be used with a class which subclasses Enum to provided translated display strings for members of the enum. The abstract properties must be overridden. To be used with `_DisplayStringEnumMixinMeta`. - Usage for python 3.7 is as follows: + Usage: ``` class ExampleEnum(_DisplayStringEnumMixin, str, Enum, metaclass=_DisplayStringEnumMixinMeta): pass diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 9d10b892520..500846c147e 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -449,12 +449,13 @@ def _run(self): timer = winUser.WinTimer(window.handle, 0, self._refreshInterval, None) self._highlighterRunningEvent.set() # notify main thread that initialisation was successful msg = MSG() - # Python 3.8 note, Change this to use an Assignment expression to catch a return value of -1. - # See the remarks section of - # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage - while winUser.getMessage(byref(msg), None, 0, 0) > 0: + while (res := winUser.getMessage(byref(msg), None, 0, 0)) > 0: winUser.user32.TranslateMessage(byref(msg)) winUser.user32.DispatchMessageW(byref(msg)) + if res == -1: + # See the return value section of + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage + raise WinError() if vision._isDebug(): log.debug("Quit message received on NVDAHighlighter thread") timer.terminate() diff --git a/source/winAPI/dpiAwareness.py b/source/winAPI/dpiAwareness.py index cfacd4cfacf..0e20fd4940c 100644 --- a/source/winAPI/dpiAwareness.py +++ b/source/winAPI/dpiAwareness.py @@ -69,19 +69,6 @@ def setDPIAwareness() -> None: # Windows 8 / Server 2012 - `shcore` library exists, # but `SetProcessDpiAwareness` is not present yet. log.debug("Cannot set PROCESS_PER_MONITOR_DPI_AWARE - SetProcessDpiAwareness missing") - except WindowsError as e: - # `ctypes` raises `WindowsError` for missing DLL's. - # On Windows 7 `shcore` library is not present. - # Inspect error code and either ignore the exception falling back to the legacy method - # if it is caused by missing dll, log an error otherwise. - # In Python 3.8 and later `ctypes` raises `FileNotFoundError` - # rather than `WindowsError` for missing libraries. - # When updating NVDA the exception has to be changed - # or, if support for Windows 7 is dropped, this section should be removed. - if e.winerror == SystemErrorCodes.MOD_NOT_FOUND: - log.debug("Cannot set PROCESS_PER_MONITOR_DPI_AWARE - shcore not found") - else: - log.error("Failed to set PROCESS_PER_MONITOR_DPI_AWARE", exc_info=True) else: if hResult == HResult.S_OK: return diff --git a/source/winUser.py b/source/winUser.py index 8e1a5d54dd5..69f42ed3c47 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -455,9 +455,11 @@ def MAKELONG(lo,hi): def waitMessage(): return user32.WaitMessage() -def getMessage(*args): + +def getMessage(*args) -> int: return user32.GetMessageW(*args) + def translateMessage(*args): return user32.TranslateMessage(*args) diff --git a/tests/unit/test_controlTypes.py b/tests/unit/test_controlTypes.py index d78776df4a1..49b8e93a1b8 100644 --- a/tests/unit/test_controlTypes.py +++ b/tests/unit/test_controlTypes.py @@ -254,23 +254,13 @@ def test_rolesValues(self): class Test_FontSize(unittest.TestCase): def test_translateFromAttribute(self): - with self.assertLogs(logHandler.log, level=logging.DEBUG) as logContext: - # We want to assert there are no logs, but the 'assertLogs' method does not support that. - # 'assertNoLogs' has been added in Python 3.10 - # Therefore, we are adding a canary warning, and then we will assert it is the only warning. - logHandler.log.debug("Canary warning") + with self.assertNoLogs(logHandler.log, level=logging.DEBUG) as logContext: # Ensure keyword sizes parse to a translatable version of themselves self.assertEqual(FontSize.translateFromAttribute("smaller"), "smaller") # Ensure measurement sizes parse to a translatable version of themselves self.assertEqual(FontSize.translateFromAttribute("13.0pt"), "13.0 pt") self.assertEqual(FontSize.translateFromAttribute("11px"), "11 px") self.assertEqual(FontSize.translateFromAttribute("23.3%"), "23.3%") - - self.assertEqual( - ["DEBUG:nvda:Canary warning"], - logContext.output, - msg="Font size parsing failed, failure was logged" - ) with self.assertLogs(logHandler.log, level=logging.DEBUG) as logContext: self.assertEqual(FontSize.translateFromAttribute("unsupported"), "unsupported") From da27960c679ae1b9735d23952c67d2ddd0898f4a Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 28 Sep 2023 12:19:16 +1000 Subject: [PATCH 2/2] update changes --- user_docs/en/changes.t2t | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index d1bce38675e..af6b22e710e 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -72,6 +72,8 @@ That method receives a ``DriverRegistrar`` object on which the ``addUsbDevices`` - ``IoThread.autoDeleteApcReference`` has been removed. (#14924, @LeonarddeR) - To support capital pitch changes, synthesizers must now explicitly declare their support for the ``PitchCommand`` in the ``supportedCommands`` attribute on the driver. (#15433, @LeonarddeR) - ``speechDictHandler.speechDictVars`` has been removed. Use ``NVDAState.WritePaths.speechDictsDir`` instead of ``speechDictHandler.speechDictVars.speechDictsPath``. (#15614, @lukaszgo1) +- ``languageHandler.makeNpgettext`` and ``languageHandler.makePgettext`` have been removed. +``npgettext`` and ``pgettext`` are supported natively now. (#15546) - === Deprecations ===