Skip to content

Commit

Permalink
AutoPropertyObject: support abstract and class properties (PR #8393)
Browse files Browse the repository at this point in the history
Closes #8294
Fixes #8652
Closes #8658

Properties and methods within classes can now be marked as abstract in NVDA. These classes will raise an error if instantiated.
See PR #8393 for full description.
  • Loading branch information
LeonarddeR authored and feerrenrut committed Dec 13, 2018
1 parent 7274f9b commit 2d8f08a
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 50 deletions.
6 changes: 3 additions & 3 deletions source/NVDAObjects/__init__.py
Expand Up @@ -6,7 +6,8 @@
#See the file COPYING for more details.

"""Module that contains the base NVDA object type"""
from new import instancemethod

from six import with_metaclass
import time
import re
import weakref
Expand Down Expand Up @@ -145,7 +146,7 @@ def clearDynamicClassCache(cls):
"""
cls._dynamicClassCache.clear()

class NVDAObject(documentBase.TextContainerObject,baseObject.ScriptableObject):
class NVDAObject(with_metaclass(DynamicNVDAObjectType, documentBase.TextContainerObject,baseObject.ScriptableObject)):
"""NVDA's representation of a single control/widget.
Every widget, regardless of how it is exposed by an application or the operating system, is represented by a single NVDAObject instance.
This allows NVDA to work with all widgets in a uniform way.
Expand All @@ -169,7 +170,6 @@ class NVDAObject(documentBase.TextContainerObject,baseObject.ScriptableObject):
An L{AppModule} can also choose overlay classes for an instance using the L{AppModule.chooseNVDAObjectOverlayClasses} method.
"""

__metaclass__=DynamicNVDAObjectType
cachePropertiesByDefault = True

#: The TextInfo class this object should use to provide access to text.
Expand Down
68 changes: 49 additions & 19 deletions source/baseObject.py
Expand Up @@ -9,31 +9,44 @@

import weakref
from logHandler import log
from abc import ABCMeta, abstractproperty
from six import with_metaclass

class Getter(object):

def __init__(self,fget):
def __init__(self,fget, abstract=False):
self.fget=fget
if abstract:
if isinstance(fget, classmethod):
log.warning("Abstract class properties are not supported.")
self._abstract = False
else:
self._abstract = self.__isabstractmethod__=abstract

def __get__(self,instance,owner):
if not instance:
if isinstance(self.fget, classmethod):
return self.fget.__get__(instance, owner)()
elif not instance:
return self
return self.fget(instance)

def setter(self,func):
return property(fget=self._func,fset=func)
return (abstractproperty if self._abstract else property)(fget=self.fget,fset=func)

def deleter(self,func):
return property(fget=self._func,fdel=func)
return (abstractproperty if self._abstract else property)(fget=self.fget,fdel=func)

class CachingGetter(Getter):

def __get__(self, instance, owner):
if not instance:
if isinstance(self.fget, classmethod):
log.warning("Class properties do not support caching")
return self.fget.__get__(instance, owner)()
elif not instance:
return self
return instance._getPropertyViaCache(self.fget)

class AutoPropertyType(type):
class AutoPropertyType(ABCMeta):

def __init__(self,name,bases,dict):
super(AutoPropertyType,self).__init__(name,bases,dict)
Expand All @@ -44,6 +57,10 @@ def __init__(self,name,bases,dict):
except KeyError:
cacheByDefault=any(getattr(base, "cachePropertiesByDefault", False) for base in bases)

# Create a set containing properties that are marked as abstract.
newAbstractProps=set()
# Create a set containing properties that were, but are no longer abstract.
oldAbstractProps=set()
# given _get_myVal, _set_myVal, and _del_myVal: "myVal" would be output 3 times
# use a set comprehension to ensure unique values, "myVal" only needs to occur once.
props={x[5:] for x in dict.keys() if x[0:5] in ('_get_','_set_','_del_')}
Expand All @@ -70,24 +87,39 @@ def __init__(self,name,bases,dict):
if cache is not None:
break
else:
cache=cacheByDefault
cache=cacheByDefault if not isinstance(g, classmethod) else False

if g and not s and not d:
setattr(self,x,(CachingGetter if cache else Getter)(g))
abstract=dict.get('_abstract_%s'%x,False)
if g and not (s or d):
attr = (CachingGetter if cache else Getter)(g,abstract)
else:
setattr(self,x,property(fget=g,fset=s,fdel=d))

class AutoPropertyObject(object):
"""A class that dynamicly supports properties, by looking up _get_* and _set_* methods at runtime.
attr = (abstractproperty if abstract else property)(fget=g,fset=s,fdel=d)
if abstract:
newAbstractProps.add(x)
elif x in self.__abstractmethods__:
oldAbstractProps.add(x)
setattr(self,x, attr)

if newAbstractProps or oldAbstractProps:
# The __abstractmethods__ set is frozen, therefore we ought to override it.
self.__abstractmethods__=(self.__abstractmethods__|newAbstractProps)-oldAbstractProps

class AutoPropertyObject(with_metaclass(AutoPropertyType, object)):
"""A class that dynamically supports properties, by looking up _get_*, _set_*, and _del_* methods at runtime.
_get_x will make property x with a getter (you can get its value).
_set_x will make a property x with a setter (you can set its value).
_del_x will make a property x with a deleter that is executed when deleting its value.
If there is a _get_x but no _set_x then setting x will override the property completely.
Properties can also be cached for the duration of one core pump cycle.
This is useful if the same property is likely to be fetched multiple times in one cycle. For example, several NVDAObject properties are fetched by both braille and speech.
Setting _cache_x to C{True} specifies that x should be cached. Setting it to C{False} specifies that it should not be cached.
This is useful if the same property is likely to be fetched multiple times in one cycle.
For example, several NVDAObject properties are fetched by both braille and speech.
Setting _cache_x to C{True} specifies that x should be cached.
Setting it to C{False} specifies that it should not be cached.
If _cache_x is not set, L{cachePropertiesByDefault} is used.
Properties can also be made abstract.
Setting _abstract_x to C{True} specifies that x should be abstract.
Setting it to C{False} specifies that it should not be abstract.
"""
__metaclass__=AutoPropertyType

#: Tracks the instances of this class; used by L{invalidateCaches}.
#: @type: weakref.WeakKeyDictionary
Expand Down Expand Up @@ -153,7 +185,7 @@ def __new__(meta, name, bases, dict):
setattr(cls, gesturesDictName, gestures)
return cls

class ScriptableObject(AutoPropertyObject):
class ScriptableObject(with_metaclass(ScriptableType, AutoPropertyObject)):
"""A class that implements NVDA's scripting interface.
Input gestures are bound to scripts such that the script will be executed when the appropriate input gesture is received.
Scripts are methods named with a prefix of C{script_}; e.g. C{script_foo}.
Expand All @@ -168,8 +200,6 @@ class ScriptableObject(AutoPropertyObject):
@type scriptCategory: basestring
"""

__metaclass__ = ScriptableType

def __init__(self):
#: Maps input gestures to script functions.
#: @type: dict
Expand Down
7 changes: 6 additions & 1 deletion source/browseMode.py
Expand Up @@ -34,6 +34,8 @@
import api
import gui.guiHelper
from NVDAObjects import NVDAObject
from abc import ABCMeta, abstractmethod
from six import with_metaclass

REASON_QUICKNAV = "quickNav"

Expand Down Expand Up @@ -91,7 +93,7 @@ def mergeQuickNavItemIterators(iterators,direction="next"):
continue
curValues.append((it,newVal))

class QuickNavItem(object):
class QuickNavItem(with_metaclass(ABCMeta, object)):
""" Emitted by L{BrowseModeTreeInterceptor._iterNodesByType}, this represents one of many positions in a browse mode document, based on the type of item being searched for (e.g. link, heading, table etc)."""

itemType=None #: The type of items searched for (e.g. link, heading, table etc)
Expand All @@ -108,6 +110,7 @@ def __init__(self,itemType,document):
self.itemType=itemType
self.document=document

@abstractmethod
def isChild(self,parent):
"""
Is this item a child of the given parent?
Expand All @@ -119,6 +122,7 @@ def isChild(self,parent):
"""
raise NotImplementedError

@abstractmethod
def report(self,readUnit=None):
"""
Reports the contents of this item.
Expand All @@ -127,6 +131,7 @@ def report(self,readUnit=None):
"""
raise NotImplementedError

@abstractmethod
def moveTo(self):
"""
Moves the browse mode caret or focus to this item.
Expand Down
9 changes: 7 additions & 2 deletions source/contentRecog/__init__.py
Expand Up @@ -15,9 +15,11 @@

from collections import namedtuple
import textInfos.offsets
from abc import ABCMeta, abstractmethod
from six import with_metaclass
from locationHelper import RectLTWH

class ContentRecognizer(object):
class ContentRecognizer(with_metaclass(ABCMeta, object)):
"""Implementation of a content recognizer.
"""

Expand All @@ -33,6 +35,7 @@ def getResizeFactor(self, width, height):
"""
return 1

@abstractmethod
def recognize(self, pixels, imageInfo, onResult):
"""Asynchronously recognize content from an image.
This method should not block.
Expand All @@ -51,6 +54,7 @@ def recognize(self, pixels, imageInfo, onResult):
"""
raise NotImplementedError

@abstractmethod
def cancel(self):
"""Cancel the recognition in progress (if any).
"""
Expand Down Expand Up @@ -125,7 +129,7 @@ def convertHeightToScreen(self, height):
"""
return int(height / self.resizeFactor)

class RecognitionResult(object):
class RecognitionResult(with_metaclass(ABCMeta, object)):
"""Provides access to the result of recognition by a recognizer.
The result is textual, but to facilitate navigation by word, line, etc.
and to allow for retrieval of screen coordinates within the text,
Expand All @@ -134,6 +138,7 @@ class RecognitionResult(object):
Most implementers should use one of the subclasses provided in this module.
"""

@abstractmethod
def makeTextInfo(self, obj, position):
"""Make a TextInfo within the recognition result text at the requested position.
@param obj: The object to return for the C{obj} property of the TextInfo.
Expand Down
7 changes: 5 additions & 2 deletions source/gui/accPropServer.py
Expand Up @@ -10,14 +10,16 @@
from comtypes.automation import VT_EMPTY
from comtypes import COMObject
from comInterfaces.Accessibility import IAccPropServer
from abc import ABCMeta, abstractmethod
from six import with_metaclass
import weakref

class IAccPropServer_Impl(COMObject):
class IAccPropServer_Impl(with_metaclass(ABCMeta, COMObject)):
"""Base class for implementing a COM interface for AccPropServer.
Please override the _GetPropValue method, not GetPropValue.
GetPropValue wraps _getPropValue to catch and log exceptions (Which for some reason NVDA's logger misses when they occur in GetPropValue).
"""

_com_interfaces_ = [
IAccPropServer
]
Expand All @@ -31,6 +33,7 @@ def __init__(self, control, *args, **kwargs):
self.control = weakref.ref(control)
super(IAccPropServer_Impl, self).__init__(*args, **kwargs)

@abstractmethod
def _getPropValue(self, pIDString, dwIDStringLen, idProp):
"""use this method to implement GetPropValue. It is wrapped by the callback GetPropValue to handle exceptions.
For instructions on implementing accPropServers, see https://msdn.microsoft.com/en-us/library/windows/desktop/dd373681(v=vs.85).aspx .
Expand Down
5 changes: 5 additions & 0 deletions source/gui/guiHelper.py
Expand Up @@ -44,6 +44,7 @@ def __init__(self,parent):
"""
import wx
from wx.lib import scrolledpanel
from abc import ABCMeta

#: border space to be used around all controls in dialogs
BORDER_FOR_DIALOGS=10
Expand Down Expand Up @@ -301,3 +302,7 @@ def addDialogDismissButtons(self, buttons):
self.dialogDismissButtonsAdded = True
return buttons

class SIPABCMeta(wx.siplib.wrappertype, ABCMeta):
"""Meta class to be used for wx subclasses with abstract methods."""
pass

9 changes: 7 additions & 2 deletions source/gui/settingsDialogs.py
Expand Up @@ -5,6 +5,8 @@
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.

from abc import abstractmethod
from six import with_metaclass
import glob
import os
import copy
Expand Down Expand Up @@ -49,7 +51,7 @@
import keyLabels
from dpiScalingHelper import DpiScalingHelperMixin

class SettingsDialog(wx.Dialog, DpiScalingHelperMixin):
class SettingsDialog(with_metaclass(guiHelper.SIPABCMeta, wx.Dialog, DpiScalingHelperMixin)):
"""A settings dialog.
A settings dialog consists of one or more settings controls and OK and Cancel buttons and an optional Apply button.
Action may be taken in response to the OK, Cancel or Apply buttons.
Expand Down Expand Up @@ -161,6 +163,7 @@ def _enterActivatesOk_ctrlSActivatesApply(self, evt):
else:
evt.Skip()

@abstractmethod
def makeSettings(self, sizer):
"""Populate the dialog with settings controls.
Subclasses must override this method.
Expand Down Expand Up @@ -205,7 +208,7 @@ def onApply(self, evt):
# redo the layout in whatever way makes sense for their particular content.
_RWLayoutNeededEvent, EVT_RW_LAYOUT_NEEDED = wx.lib.newevent.NewCommandEvent()

class SettingsPanel(wx.Panel, DpiScalingHelperMixin):
class SettingsPanel(with_metaclass(guiHelper.SIPABCMeta, wx.Panel, DpiScalingHelperMixin)):
"""A settings panel, to be used in a multi category settings dialog.
A settings panel consists of one or more settings controls.
Action may be taken in response to the parent dialog's OK or Cancel buttons.
Expand Down Expand Up @@ -243,6 +246,7 @@ def __init__(self, parent):
if gui._isDebug():
log.debug("Loading %s took %.2f seconds"%(self.__class__.__name__, time.time() - startTime))

@abstractmethod
def makeSettings(self, sizer):
"""Populate the panel with settings controls.
Subclasses must override this method.
Expand All @@ -264,6 +268,7 @@ def onPanelDeactivated(self):
"""
self.Hide()

@abstractmethod
def onSave(self):
"""Take action in response to the parent's dialog OK or apply button being pressed.
Sub-classes should override this method.
Expand Down
1 change: 1 addition & 0 deletions source/inputCore.py
Expand Up @@ -64,6 +64,7 @@ class InputGesture(baseObject.AutoPropertyObject):
#: @type: bool
reportInInputHelp=True

_abstract_identifiers = True
def _get_identifiers(self):
"""The identifier(s) which will be used in input gesture maps to represent this gesture.
These identifiers will be normalized and looked up in order until a match is found.
Expand Down

0 comments on commit 2d8f08a

Please sign in to comment.