Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic framework for code extensibility via actions and filters #7484

Merged
merged 4 commits into from Sep 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions source/audioDucking.py
Expand Up @@ -117,6 +117,7 @@ def initialize():
return
_setDuckingState(False)
setAudioDuckingMode(config.conf['audio']['audioDuckingMode'])
config.configProfileSwitched.register(handleConfigProfileSwitch)

_isAudioDuckingSupported=None
def isAudioDuckingSupported():
Expand All @@ -126,8 +127,7 @@ def isAudioDuckingSupported():
return _isAudioDuckingSupported

def handleConfigProfileSwitch():
if isAudioDuckingSupported():
setAudioDuckingMode(config.conf['audio']['audioDuckingMode'])
setAudioDuckingMode(config.conf['audio']['audioDuckingMode'])

class AudioDucker(object):
""" Create one of these objects to manage ducking of background audio.
Expand Down
2 changes: 2 additions & 0 deletions source/braille.py
Expand Up @@ -1426,6 +1426,7 @@ def __init__(self):
self._cursorBlinkUp = True
self._cells = []
self._cursorBlinkTimer = None
config.configProfileSwitched.register(self.handleConfigProfileSwitch)

def terminate(self):
if self._messageCallLater:
Expand All @@ -1434,6 +1435,7 @@ def terminate(self):
if self._cursorBlinkTimer:
self._cursorBlinkTimer.Stop()
self._cursorBlinkTimer = None
config.configProfileSwitched.unregister(self.handleConfigProfileSwitch)
if self.display:
self.display.terminate()
self.display = None
Expand Down
1 change: 1 addition & 0 deletions source/brailleInput.py
Expand Up @@ -88,6 +88,7 @@ def __init__(self):
self.untranslatedCursorPos = 0
#: The time at which uncontracted characters were sent to the system.
self._uncontSentTime = None
config.configProfileSwitched.register(self.handleConfigProfileSwitch)

@property
def table(self):
Expand Down
16 changes: 8 additions & 8 deletions source/config/__init__.py
Expand Up @@ -27,13 +27,20 @@
import easeOfAccess
from fileUtils import FaultTolerantFile
import winKernel
import extensionPoints
import profileUpgrader
from .configSpec import confspec

#: The active configuration, C{None} if it has not yet been loaded.
#: @type: ConfigObj
conf = None

#: Notifies when the configuration profile is switched.
#: This allows components to apply changes required by the new configuration.
#: For example, braille switches braille displays if necessary.
#: Handlers are called with no arguments.
configProfileSwitched = extensionPoints.Action()

def initialize():
global conf
conf = ConfigManager()
Expand Down Expand Up @@ -333,14 +340,7 @@ def _handleProfileSwitch(self):
if init:
# We're still initialising, so don't notify anyone about this change.
return
import synthDriverHandler
synthDriverHandler.handleConfigProfileSwitch()
import brailleInput
brailleInput.handler.handleConfigProfileSwitch()
import braille
braille.handler.handleConfigProfileSwitch()
import audioDucking
audioDucking.handleConfigProfileSwitch()
configProfileSwitched.notify()

def _initBaseConf(self, factoryDefaults=False):
fn = os.path.join(globalVars.appArgs.configPath, "nvda.ini")
Expand Down
250 changes: 250 additions & 0 deletions source/extensionPoints.py
@@ -0,0 +1,250 @@
#extensionPoints.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2017 NV Access Limited
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.

"""Framework to enable extensibility at specific points in the code.
This allows interested parties to register to be notified when some action occurs
or to modify a specific kind of data.
For example, you might wish to notify about a configuration profile switch
or allow modification of spoken messages before they are passed to the synthesizer.
See the L{Action} and L{Filter} classes.
"""

import weakref
import collections
import inspect
from logHandler import log

class AnnotatableWeakref(weakref.ref):
"""A weakref.ref which allows annotation with custom attributes.
"""

class BoundMethodWeakref(object):
"""Weakly references a bound instance method.
Instance methods are bound dynamically each time they are fetched.
weakref.ref on a bound instance method doesn't work because
as soon as you drop the reference, the method object dies.
Instead, this class holds weak references to both the instance and the function,
which can then be used to bind an instance method.
To get the actual method, you call an instance as you would a weakref.ref.
"""

def __init__(self, target, onDelete):
def onRefDelete(weak):
"""Calls onDelete for our BoundMethodWeakref when one of the individual weakrefs (instance or function) dies.
"""
onDelete(self)
inst = target.__self__
func = target.__func__
self.weakInst = weakref.ref(inst, onRefDelete)
self.weakFunc = weakref.ref(func, onRefDelete)

def __call__(self):
inst = self.weakInst()
if not inst:
return
func = self.weakFunc()
assert func, "inst is alive but func is dead"
# Get an instancemethod by binding func to inst.
return func.__get__(inst)

def _getHandlerKey(handler):
"""Get a key which identifies a handler function.
This is needed because we store weak references, not the actual functions.
We store the key on the weak reference.
When the handler dies, we can use the key on the weak reference to remove the handler.
"""
inst = getattr(handler, "__self__", None)
if inst:
return (id(inst), id(handler.__func__))
return id(handler)

class HandlerRegistrar(object):
"""Base class to Facilitate registration and unregistration of handler functions.
The handlers are stored using weak references and are automatically unregistered
if the handler dies.
Both normal functions and instance methods are supported.
The handlers are maintained in the order they were registered
so that they can be called in a deterministic order across runs.
This class doesn't provide any functionality to actually call the handlers.
If you want to implement an extension point,
you probably want the L{Action} or L{Filter} subclasses instead.
"""

def __init__(self):
#: Registered handler functions.
#: This is an OrderedDict where the keys are unique identifiers (as returned by _getHandlerKey)
#: and the values are weak references.
self._handlers = collections.OrderedDict()

def register(self, handler):
if hasattr(handler, "__self__"):
weak = BoundMethodWeakref(handler, self.unregister)
else:
weak = AnnotatableWeakref(handler, self.unregister)
key = _getHandlerKey(handler)
# Store the key on the weakref so we can remove the handler when it dies.
weak.handlerKey = key
self._handlers[key] = weak

def unregister(self, handler):
if isinstance(handler, (AnnotatableWeakref, BoundMethodWeakref)):
key = handler.handlerKey
else:
key = _getHandlerKey(handler)
try:
del self._handlers[key]
except KeyError:
return False
return True

@property
def handlers(self):
"""Generator of registered handler functions.
This should be used when you want to call the handlers.
"""
for weak in self._handlers.values():
handler = weak()
if not handler:
continue # Died.
yield handler

def callWithSupportedKwargs(func, *args, **kwargs):
"""Call a function with only the keyword arguments it supports.
For example, if myFunc is defined as:
def myFunc(a=None, b=None):
and you call:
callWithSupportedKwargs(myFunc, a=1, b=2, c=3)
Instead of raising a TypeError, myFunc will simply be called like this:
myFunc(a=1, b=2)
"""
spec = inspect.getargspec(func)
if spec.keywords:
# func has a catch-all for kwargs (**kwargs).
return func(*args, **kwargs)
# spec.defaults lists all arguments with defaults (keyword arguments).
numKwargs = len(spec.defaults) if spec.defaults else 0
# spec.args lists all arguments, first positional, then keyword.
firstKwarg = len(spec.args) - numKwargs
supportedKwargs = set(spec.args[firstKwarg:])
for kwarg in kwargs.keys():
if kwarg not in supportedKwargs:
del kwargs[kwarg]
return func(*args, **kwargs)

class Action(HandlerRegistrar):
"""Allows interested parties to register to be notified when some action occurs.
For example, this might be used to notify that the configuration profile has been switched.

First, an Action is created:

>>> somethingHappened = extensionPoints.Action()

Interested parties then register to be notified about this action:

>>> def onSomethingHappened(someArg=None):
... print(someArg)
...
>>> somethingHappened.register(onSomethingHappened)

When the action is performed, register handlers are notified:

>>> somethingHappened.notify(someArg=42)
"""

def notify(self, **kwargs):
"""Notify all registered handlers that the action has occurred.
@param kwargs: Arguments to pass to the handlers.
"""
for handler in self.handlers:
try:
callWithSupportedKwargs(handler, **kwargs)
except:
log.exception("Error running handler %r for %r" % (handler, self))

class Filter(HandlerRegistrar):
"""Allows interested parties to register to modify a specific kind of data.
For example, this might be used to allow modification of spoken messages before they are passed to the synthesizer.

First, a Filter is created:

>>> messageFilter = extensionPoints.Filter()

Interested parties then register to filter the data:

>>> def filterMessage(message, someArg=None):
... return message + " which has been filtered."
...
>>> messageFilter.register(filterMessage)

When filtering is desired, all registered handlers are called to filter the data:

>>> messageFilter.apply("This is a message", someArg=42)
'This is a message which has been filtered'
"""

def apply(self, value, **kwargs):
"""Pass a value to be filtered through all registered handlers.
The value is passed to the first handler
and the return value from that handler is passed to the next handler.
This process continues for all handlers until the final handler.
The return value from the final handler is returned to the caller.
@param value: The value to be filtered.
@param kwargs: Arguments to pass to the handlers.
@return: The filtered value.
"""
for handler in self.handlers:
try:
value = callWithSupportedKwargs(handler, value, **kwargs)
except:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be allow an optional exceptionsShouldAbort argument in the constructor of Filters, which defaults to False? I can think of situations where filters depend on each other, and when one filter handler fails, it results in unexpected behavior when the subsequent handlers are allowed to run.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filters shouldn't explicitly depend on each other. That just isn't possible in an ecosystem where you can't know what filters will be installed. Of course an earlier filter affects a later filter by changing the value, but the later filter should not depend on specific behaviour provided by a particular earlier filter. For all you know, the earlier filter didn't get registered or a filter got registered in between them. If you want that kind of explicit dependency, the correct solution is to use two filters; e.g. a pre-process filter and a post-process filter.

log.exception("Error running handler %r for %r" % (handler, self))
return value

class Decider(HandlerRegistrar):
"""Allows interested parties to participate in deciding whether something
should be done.
For example, input gestures are normally executed,
but this might be used to prevent their execution
under specific circumstances such as when controlling a remote system.

First, a Decider is created:

>>> doSomething = extensionPoints.Decider()

Interested parties then register to participate in the decision:

>>> def shouldDoSomething(someArg=None):
... return False
...
>>> doSomething.register(shouldDoSomething)

When the decision is to be made, registered handlers are called until
a handler returns False:

>>> doSomething.decide(someArg=42)
False

If there are no handlers or all handlers return True,
the return value is True.
"""

def decide(self, **kwargs):
"""Call handlers to make a decision.
If a handler returns False, processing stops
and False is returned.
If there are no handlers or all handlers return True, True is returned.
@param kwargs: Arguments to pass to the handlers.
@return: The decision.
@rtype: bool
"""
for handler in self.handlers:
try:
decision = callWithSupportedKwargs(handler, **kwargs)
except:
log.exception("Error running handler %r for %r" % (handler, self))
continue
if not decision:
return False
return True
3 changes: 2 additions & 1 deletion source/synthDriverHandler.py
Expand Up @@ -3,7 +3,7 @@
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2006-2015 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Joseph Lee
#Copyright (C) 2006-2017 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Joseph Lee

from copy import deepcopy
import os
Expand All @@ -22,6 +22,7 @@

def initialize():
config.addConfigDirsToPythonPackagePath(synthDrivers)
config.configProfileSwitched.register(handleConfigProfileSwitch)

def changeVoice(synth, voice):
# This function can be called with no voice if the synth doesn't support the voice setting (only has one voice).
Expand Down