Skip to content

Commit 35813ea

Browse files
authored
Merge 75d273a into e86614f
2 parents e86614f + 75d273a commit 35813ea

8 files changed

Lines changed: 543 additions & 163 deletions

File tree

source/bdDetect.py

Lines changed: 200 additions & 132 deletions
Large diffs are not rendered by default.

source/braille.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2565,7 +2565,8 @@ def _enableDetection(
25652565
self._detector.rescan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
25662566
return
25672567
config.conf["braille"]["display"] = AUTO_DISPLAY_NAME
2568-
self._detector = bdDetect.Detector(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
2568+
self._detector = bdDetect._Detector()
2569+
self._detector._queueBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
25692570

25702571
def _disableDetection(self):
25712572
"""Disables automatic detection of braille displays."""
@@ -2640,7 +2641,6 @@ def initialize():
26402641
newTableName = brailleTables.RENAMED_TABLES.get(oldTableName)
26412642
if newTableName:
26422643
config.conf["braille"]["translationTable"] = newTableName
2643-
bdDetect.initializeDetectionData()
26442644
handler = BrailleHandler()
26452645
# #7459: the syncBraille has been dropped in favor of the native hims driver.
26462646
# Migrate to renamed drivers as smoothly as possible.

source/core.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ def resetConfiguration(factoryDefaults=False):
212212
import speech
213213
import vision
214214
import inputCore
215+
import bdDetect
215216
import hwIo
216217
import tones
217218
log.debug("Terminating vision")
@@ -224,6 +225,8 @@ def resetConfiguration(factoryDefaults=False):
224225
speech.terminate()
225226
log.debug("terminating tones")
226227
tones.terminate()
228+
log.debug("Terminating background braille display detection")
229+
bdDetect.terminate()
227230
log.debug("Terminating background i/o")
228231
hwIo.terminate()
229232
log.debug("terminating addonHandler")
@@ -243,6 +246,8 @@ def resetConfiguration(factoryDefaults=False):
243246
# Hardware background i/o
244247
log.debug("initializing background i/o")
245248
hwIo.initialize()
249+
log.debug("Initializing background braille display detection")
250+
bdDetect.initialize()
246251
# Tones
247252
tones.initialize()
248253
#Speech
@@ -521,6 +526,9 @@ def main():
521526
log.debug("initializing background i/o")
522527
import hwIo
523528
hwIo.initialize()
529+
log.debug("Initializing background braille display detection")
530+
import bdDetect
531+
bdDetect.initialize()
524532
log.debug("Initializing tones")
525533
import tones
526534
tones.initialize()
@@ -791,6 +799,7 @@ def _doPostNvdaStartupAction():
791799
_terminate(brailleInput)
792800
_terminate(braille)
793801
_terminate(speech)
802+
_terminate(bdDetect)
794803
_terminate(hwIo)
795804
_terminate(addonHandler)
796805
_terminate(garbageHandler)

source/extensionPoints/__init__.py

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2017-2021 NV Access Limited, Joseph Lee, Łukasz Golonka, Leonard de Ruijter
2+
# Copyright (C) 2017-2023 NV Access Limited, Joseph Lee, Łukasz Golonka, Leonard de Ruijter
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

@@ -13,13 +13,17 @@
1313
from logHandler import log
1414
from .util import HandlerRegistrar, callWithSupportedKwargs, BoundMethodWeakref
1515
from typing import (
16+
Callable,
17+
Generator,
1618
Generic,
19+
Iterable,
1720
Set,
1821
TypeVar,
22+
Union,
1923
)
2024

2125

22-
class Action(HandlerRegistrar):
26+
class Action(HandlerRegistrar[Callable[..., None]]):
2327
"""Allows interested parties to register to be notified when some action occurs.
2428
For example, this might be used to notify that the configuration profile has been switched.
2529
@@ -69,19 +73,23 @@ def notifyOnce(self, **kwargs):
6973
FilterValueT = TypeVar("FilterValueT")
7074

7175

72-
class Filter(HandlerRegistrar, Generic[FilterValueT]):
76+
class Filter(
77+
HandlerRegistrar[Union[Callable[..., FilterValueT], Callable[[FilterValueT], FilterValueT]]],
78+
Generic[FilterValueT]
79+
):
7380
"""Allows interested parties to register to modify a specific kind of data.
7481
For example, this might be used to allow modification of spoken messages before they are passed to the synthesizer.
7582
7683
First, a Filter is created:
7784
78-
>>> messageFilter = extensionPoints.Filter()
85+
>>> import extensionPoints
86+
>>> messageFilter = extensionPoints.Filter[str]()
7987
8088
Interested parties then register to filter the data, see
8189
L{register} docstring for details of the type of handlers that can be
8290
registered:
8391
84-
>>> def filterMessage(message, someArg=None):
92+
>>> def filterMessage(message: str, someArg=None) -> str:
8593
... return message + " which has been filtered."
8694
...
8795
>>> messageFilter.register(filterMessage)
@@ -111,7 +119,7 @@ def apply(self, value: FilterValueT, **kwargs) -> FilterValueT:
111119
return value
112120

113121

114-
class Decider(HandlerRegistrar):
122+
class Decider(HandlerRegistrar[Callable[..., bool]]):
115123
"""Allows interested parties to participate in deciding whether something
116124
should be done.
117125
For example, input gestures are normally executed,
@@ -162,7 +170,7 @@ def decide(self, **kwargs):
162170
return True
163171

164172

165-
class AccumulatingDecider(HandlerRegistrar):
173+
class AccumulatingDecider(HandlerRegistrar[Callable[..., bool]]):
166174
"""Allows interested parties to participate in deciding whether something
167175
should be done.
168176
In contrast with L{Decider} all handlers are executed and then results are returned.
@@ -216,3 +224,54 @@ def decide(self, **kwargs) -> bool:
216224
if (not self.defaultDecision) in decisions:
217225
return (not self.defaultDecision)
218226
return self.defaultDecision
227+
228+
229+
ChainValueTypeT = TypeVar("ChainValueTypeT")
230+
231+
232+
class Chain(HandlerRegistrar[Callable[..., Iterable[ChainValueTypeT]]], Generic[ChainValueTypeT]):
233+
"""Allows creating a chain of registered handlers.
234+
The handlers should return an iterable, e.g. they are usually generator functions,
235+
but returning a list is also supported.
236+
237+
First, a Chain is created:
238+
239+
>>> chainOfNumbers = extensionPoints.Chain[int]()
240+
241+
Interested parties then register to be iterated.
242+
See L{register} docstring for details of the type of handlers that can be
243+
registered:
244+
245+
>>> def yieldSomeNumbers(someArg=None) -> Generator[int, None, None]:
246+
... yield 1
247+
... yield 2
248+
... yield 3
249+
...
250+
>>> def yieldMoreNumbers(someArg=42) -> Generator[int, None, None]:
251+
... yield 4
252+
... yield 5
253+
... yield 6
254+
...
255+
>>> chainOfNumbers.register(yieldSomeNumbers)
256+
>>> chainOfNumbers.register(yieldMoreNumbers)
257+
258+
When the chain is being iterated, it yields all entries generated by the registered handlers,
259+
see L{util.callWithSupportedKwargs} for how args passed to iter are mapped to the handler:
260+
261+
>>> chainOfNumbers.iter(someArg=42)
262+
"""
263+
264+
def iter(self, **kwargs) -> Generator[ChainValueTypeT, None, None]:
265+
"""Returns a generator yielding all values generated by the registered handlers.
266+
@param kwargs: Arguments to pass to the handlers.
267+
"""
268+
for handler in self.handlers:
269+
try:
270+
iterable = callWithSupportedKwargs(handler, **kwargs)
271+
if not isinstance(iterable, Iterable):
272+
log.exception(f"The handler {handler!r} on {self!r} didn't return an iterable")
273+
continue
274+
for value in iterable:
275+
yield value
276+
except Exception:
277+
log.exception(f"Error yielding value from handler {handler!r} for {self!r}")

source/extensionPoints/util.py

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
1-
#util.py
2-
#A part of NonVisual Desktop Access (NVDA)
3-
#Copyright (C) 2017 NV Access Limited
4-
#This file is covered by the GNU General Public License.
5-
#See the file COPYING for more details.
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# Copyright (C) 2017-2023 NV Access Limited, Leonard de Ruijter
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
65

76
"""Utilities used withing the extension points framework. Generally it is expected that the class in __init__.py are
87
used, however for more advanced requirements these utilities can be used directly.
98
"""
9+
1010
import weakref
11-
import collections
1211
import inspect
13-
14-
15-
class AnnotatableWeakref(weakref.ref):
12+
from typing import (
13+
Callable,
14+
Generator,
15+
Generic,
16+
Optional,
17+
OrderedDict,
18+
Tuple,
19+
TypeVar,
20+
Union,
21+
)
22+
23+
HandlerT = TypeVar("HandlerT", bound=Callable)
24+
HandlerKeyT = Union[int, Tuple[int, int]]
25+
26+
27+
class AnnotatableWeakref(weakref.ref, Generic[HandlerT]):
1628
"""A weakref.ref which allows annotation with custom attributes.
1729
"""
30+
handlerKey: int
1831

1932

20-
class BoundMethodWeakref(object):
33+
class BoundMethodWeakref(Generic[HandlerT]):
2134
"""Weakly references a bound instance method.
2235
Instance methods are bound dynamically each time they are fetched.
2336
weakref.ref on a bound instance method doesn't work because
@@ -26,8 +39,9 @@ class BoundMethodWeakref(object):
2639
which can then be used to bind an instance method.
2740
To get the actual method, you call an instance as you would a weakref.ref.
2841
"""
42+
handlerKey: Tuple[int, int]
2943

30-
def __init__(self, target, onDelete):
44+
def __init__(self, target: HandlerT, onDelete):
3145
def onRefDelete(weak):
3246
"""Calls onDelete for our BoundMethodWeakref when one of the individual weakrefs (instance or function) dies.
3347
"""
@@ -37,7 +51,7 @@ def onRefDelete(weak):
3751
self.weakInst = weakref.ref(inst, onRefDelete)
3852
self.weakFunc = weakref.ref(func, onRefDelete)
3953

40-
def __call__(self):
54+
def __call__(self) -> Optional[HandlerT]:
4155
inst = self.weakInst()
4256
if not inst:
4357
return
@@ -46,7 +60,8 @@ def __call__(self):
4660
# Get an instancemethod by binding func to inst.
4761
return func.__get__(inst)
4862

49-
def _getHandlerKey(handler):
63+
64+
def _getHandlerKey(handler: HandlerT) -> HandlerKeyT:
5065
"""Get a key which identifies a handler function.
5166
This is needed because we store weak references, not the actual functions.
5267
We store the key on the weak reference.
@@ -58,7 +73,7 @@ def _getHandlerKey(handler):
5873
return id(handler)
5974

6075

61-
class HandlerRegistrar(object):
76+
class HandlerRegistrar(Generic[HandlerT]):
6277
"""Base class to Facilitate registration and unregistration of handler functions.
6378
The handlers are stored using weak references and are automatically unregistered
6479
if the handler dies.
@@ -75,12 +90,16 @@ def __init__(self):
7590
#: Registered handler functions.
7691
#: This is an OrderedDict where the keys are unique identifiers (as returned by _getHandlerKey)
7792
#: and the values are weak references.
78-
self._handlers = collections.OrderedDict()
93+
self._handlers = OrderedDict[
94+
HandlerKeyT,
95+
Union[BoundMethodWeakref[HandlerT], AnnotatableWeakref[HandlerT]]
96+
]()
7997

80-
def register(self, handler):
98+
def register(self, handler: HandlerT):
8199
"""You can register functions, bound instance methods, class methods, static methods or lambdas.
82-
However, the callable must be kept alive by your code otherwise it will be de-registered. This is due to the use
83-
of weak references. This is especially relevant when using lambdas.
100+
However, the callable must be kept alive by your code otherwise it will be de-registered.
101+
This is due to the use of weak references.
102+
This is especially relevant when using lambdas.
84103
"""
85104
if inspect.isfunction(handler):
86105
sig = inspect.signature(handler)
@@ -95,7 +114,24 @@ def register(self, handler):
95114
weak.handlerKey = key
96115
self._handlers[key] = weak
97116

98-
def unregister(self, handler):
117+
def moveToEnd(self, handler: HandlerT, last: bool = False) -> bool:
118+
"""Move a registered handler to the start or end of the collection with registered handlers.
119+
This can be used to modify the order in which handlers are called.
120+
@param last: Whether to move the handler to the end.
121+
If C{False} (default), the handler is moved to the start.
122+
@returns: Whether the handler was found.
123+
"""
124+
if isinstance(handler, (AnnotatableWeakref, BoundMethodWeakref)):
125+
key = handler.handlerKey
126+
else:
127+
key = _getHandlerKey(handler)
128+
try:
129+
self._handlers.move_to_end(key=key, last=last)
130+
except KeyError:
131+
return False
132+
return True
133+
134+
def unregister(self, handler: Union[AnnotatableWeakref[HandlerT], BoundMethodWeakref[HandlerT], HandlerT]):
99135
if isinstance(handler, (AnnotatableWeakref, BoundMethodWeakref)):
100136
key = handler.handlerKey
101137
else:
@@ -107,7 +143,7 @@ def unregister(self, handler):
107143
return True
108144

109145
@property
110-
def handlers(self):
146+
def handlers(self) -> Generator[HandlerT, None, None]:
111147
"""Generator of registered handler functions.
112148
This should be used when you want to call the handlers.
113149
"""

tests/unit/extensionPointTestHelpers.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@
55

66
"""Helper functions to test extension points."""
77

8-
from extensionPoints import Action, Decider, Filter, FilterValueT
8+
from extensionPoints import (
9+
Action,
10+
Chain,
11+
ChainValueTypeT,
12+
Decider,
13+
Filter,
14+
FilterValueT,
15+
)
916
import unittest
1017
from contextlib import contextmanager
18+
from typing import Iterable
1119

1220

1321
@contextmanager
@@ -87,7 +95,7 @@ def filterTester(
8795
useAssertDictContainsSubset: bool = False,
8896
**expectedKwargs
8997
):
90-
"""A function that allows testing a Filter.
98+
"""A context manager that allows testing a Filter.
9199
@param testCase: The test case to apply the assertion on.
92100
@param filter: The filter that will be applied by the test case.
93101
@param expectedInput: The expected input as entering the filter handler.
@@ -116,3 +124,39 @@ def handler(value: FilterValueT, **kwargs):
116124
filter.unregister(handler)
117125
testFunc = testCase.assertDictContainsSubset if useAssertDictContainsSubset else testCase.assertDictEqual
118126
testFunc(expectedKwargs, actualKwargs)
127+
128+
129+
@contextmanager
130+
def chainTester(
131+
testCase: unittest.TestCase,
132+
chain: Chain,
133+
expectedOutput: Iterable[ChainValueTypeT],
134+
useAssertDictContainsSubset: bool = False,
135+
**expectedKwargs
136+
):
137+
"""A context manager that allows testing a Filter.
138+
@param testCase: The test case to apply the assertion on.
139+
@param chain: The Chain that will be iterated by the test case.
140+
@param expectedOutput: The expected output as returned by L{Chain.iter}
141+
it will also be yielded by the context manager
142+
@param useAssertDictContainsSubset: Whether to use L{unittest.TestCase.assertDictContainsSubset} instead of
143+
L{unittest.TestCase.assertDictEqual}
144+
This can be used if a Chain is iterated with dictionary values that can't be predicted at test time,
145+
such as a driver instance.
146+
@param expectedKwargs: The kwargs that are expected to be passed to the Chain handler.
147+
"""
148+
expectedKwargs["_called"] = True
149+
actualKwargs = {}
150+
151+
def handler(**kwargs):
152+
actualKwargs.update(kwargs)
153+
actualKwargs["_called"] = True
154+
return expectedOutput
155+
156+
chain.register(handler)
157+
try:
158+
yield expectedOutput
159+
finally:
160+
chain.unregister(handler)
161+
testFunc = testCase.assertDictContainsSubset if useAssertDictContainsSubset else testCase.assertDictEqual
162+
testFunc(expectedKwargs, actualKwargs)

0 commit comments

Comments
 (0)