diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index 2b33b5d1c28..76e0047d0a3 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -37,7 +37,7 @@ import UIAUtils from comInterfaces import UIAutomationClient as UIA # F403: unable to detect undefined names -from comInterfaces .UIAutomationClient import * # noqa: F403 +from comInterfaces.UIAutomationClient import * # noqa: F403 import textInfos from typing import Dict from queue import Queue diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 43b1c80b5d4..b812bab9324 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -5,6 +5,7 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. +import typing import time import os import sys @@ -24,6 +25,7 @@ import queueHandler import core from . import guiHelper +from . import settingsDialogs from .settingsDialogs import * from .inputGestures import InputGesturesDialog import speechDictHandler @@ -585,10 +587,16 @@ def terminate(): import brailleViewer brailleViewer.destroyBrailleViewer() - for instance, state in gui.SettingsDialog._instances.items(): - if state is gui.SettingsDialog._DIALOG_DESTROYED_STATE: + # prevent race condition with object deletion + # prevent deletion of the object while we work on it. + _SettingsDialog = settingsDialogs.SettingsDialog + nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) + + for instance, state in nonWeak.items(): + if state is _SettingsDialog.DialogState.DESTROYED: log.error( - "Destroyed but not deleted instance of settings dialog exists: {!r}".format(instance) + "Destroyed but not deleted instance of gui.SettingsDialog exists" + f": {instance.title} - {instance.__class__.__qualname__} - {instance}" ) else: log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 37c306f0ce7..7201aa2a323 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -9,7 +9,9 @@ from abc import ABCMeta, abstractmethod import copy import os +from enum import IntEnum +import typing import wx from vision.providerBase import VisionEnhancementProviderSettings from wx.lib import scrolledpanel @@ -80,10 +82,12 @@ class SettingsDialog( class MultiInstanceError(RuntimeError): pass - _DIALOG_CREATED_STATE = 0 - _DIALOG_DESTROYED_STATE = 1 + class DialogState(IntEnum): + CREATED = 0 + DESTROYED = 1 + # holds instances of SettingsDialogs as keys, and state as the value - _instances=weakref.WeakKeyDictionary() + _instances = weakref.WeakKeyDictionary() title = "" helpId = "NVDASettings" shouldSuspendConfigProfileTriggers = True @@ -102,25 +106,39 @@ def __new__(cls, *args, **kwargs): "Creating new settings dialog (multiInstanceAllowed:{}). " "State of _instances {!r}".format(multiInstanceAllowed, instancesState) ) - if state is cls._DIALOG_CREATED_STATE and not multiInstanceAllowed: + if state is cls.DialogState.CREATED and not multiInstanceAllowed: raise SettingsDialog.MultiInstanceError("Only one instance of SettingsDialog can exist at a time") - if state is cls._DIALOG_DESTROYED_STATE and not multiInstanceAllowed: + if state is cls.DialogState.DESTROYED and not multiInstanceAllowed: # the dialog has been destroyed by wx, but the instance is still available. This indicates there is something # keeping it alive. log.error("Opening new settings dialog while instance still exists: {!r}".format(firstMatchingInstance)) obj = super(SettingsDialog, cls).__new__(cls, *args, **kwargs) - SettingsDialog._instances[obj] = cls._DIALOG_CREATED_STATE + SettingsDialog._instances[obj] = cls.DialogState.CREATED return obj def _setInstanceDestroyedState(self): - if log.isEnabledFor(log.DEBUG): - instancesState = dict(SettingsDialog._instances) - log.debug( - "Setting state to destroyed for instance: {!r}\n" - "Current _instances {!r}".format(self, instancesState) - ) - if self in SettingsDialog._instances: - SettingsDialog._instances[self] = self._DIALOG_DESTROYED_STATE + # prevent race condition with object deletion + # prevent deletion of the object while we work on it. + nonWeak: typing.Dict[SettingsDialog, SettingsDialog.DialogState] = dict(SettingsDialog._instances) + + if ( + self in SettingsDialog._instances + # Because destroy handlers are use evt.skip, _setInstanceDestroyedState may be called many times + # prevent noisy logging. + and self.DialogState.DESTROYED != SettingsDialog._instances[self] + ): + if log.isEnabledFor(log.DEBUG): + instanceStatesGen = ( + f"{instance.title} - {state.name}" + for instance, state in nonWeak.items() + ) + instancesList = list(instanceStatesGen) + log.debug( + f"Setting state to destroyed for instance: {self.title} - {self.__class__.__qualname__} - {self}\n" + f"Current _instances {instancesList}" + ) + SettingsDialog._instances[self] = self.DialogState.DESTROYED + def __init__( self, parent,