def test_dictionary_collection_longest_key(self): k1 = ('S', ) k2 = ('S', 'T') k3 = ('S', 'T', 'R') dc = StenoDictionaryCollection() self.assertEqual(dc.longest_key, 0) d1 = StenoDictionary() d1._path = 'd1' d1.save = lambda: None d1[k1] = 'a' dc.set_dicts([d1]) self.assertEqual(dc.longest_key, 1) d1[k2] = 'a' self.assertEqual(dc.longest_key, 2) d2 = StenoDictionary() d2._path = 'd2' d2[k3] = 'c' dc.set_dicts([d1, d2]) self.assertEqual(dc.longest_key, 3) del d1[k2] self.assertEqual(dc.longest_key, 3) dc.set_dicts([d1]) self.assertEqual(dc.longest_key, 1) dc.set_dicts([]) self.assertEqual(dc.longest_key, 0)
class TranslatorStateSizeTestCase(unittest.TestCase): class FakeState(_State): def __init__(self): _State.__init__(self) self.restrict_calls = [] def restrict_size(self, n): self.restrict_calls.append(n) def assert_size_call(self, size): self.assertEqual(self.s.restrict_calls[-1], size) def assert_no_size_call(self): self.assertEqual(self.s.restrict_calls, []) def clear(self): self.s.restrict_calls = [] def setUp(self): self.t = Translator() self.s = type(self).FakeState() self.t._state = self.s self.d = StenoDictionary() self.dc = StenoDictionaryCollection() self.dc.set_dicts([self.d]) self.t.set_dictionary(self.dc) def test_dictionary_update_grows_size1(self): self.d[('S', )] = '1' self.assert_size_call(1) def test_dictionary_update_grows_size4(self): self.d[('S', 'PT', '-Z', 'TOP')] = 'hi' self.assert_size_call(4) def test_dictionary_update_no_grow(self): self.t.set_min_undo_length(4) self.assert_size_call(4) self.clear() self.d[('S', 'T')] = 'nothing' self.assert_size_call(4) def test_dictionary_update_shrink(self): self.d[('S', 'T', 'P', '-Z', '-D')] = '1' self.assert_size_call(5) self.clear() self.d[('A', 'P')] = '2' self.assert_no_size_call() del self.d[('S', 'T', 'P', '-Z', '-D')] self.assert_size_call(2) def test_dictionary_update_no_shrink(self): self.t.set_min_undo_length(7) self.d[('S', 'T', 'P', '-Z', '-D')] = '1' del self.d[('S', 'T', 'P', '-Z', '-D')] self.assert_size_call(7) def test_translation_calls_restrict(self): self.t.translate(stroke('S')) self.assert_size_call(0)
def test_dictionary_collection_longest_key(): k1 = ('S',) k2 = ('S', 'T') k3 = ('S', 'T', 'R') dc = StenoDictionaryCollection() assert dc.longest_key == 0 d1 = StenoDictionary() d1.path = 'd1' d1[k1] = 'a' dc.set_dicts([d1]) assert dc.longest_key == 1 d1[k2] = 'a' assert dc.longest_key == 2 d2 = StenoDictionary() d2.path = 'd2' d2[k3] = 'c' dc.set_dicts([d2, d1]) assert dc.longest_key == 3 del d1[k2] assert dc.longest_key == 3 dc.set_dicts([d1]) assert dc.longest_key == 1 dc.set_dicts([]) assert dc.longest_key == 0
class TranslatorStateSizeTestCase(unittest.TestCase): class FakeState(_State): def __init__(self): _State.__init__(self) self.restrict_calls = [] def restrict_size(self, n): self.restrict_calls.append(n) def assert_size_call(self, size): self.assertEqual(self.s.restrict_calls[-1], size) def assert_no_size_call(self): self.assertEqual(self.s.restrict_calls, []) def clear(self): self.s.restrict_calls = [] def setUp(self): self.t = Translator() self.s = type(self).FakeState() self.t._state = self.s self.d = StenoDictionary() self.dc = StenoDictionaryCollection() self.dc.set_dicts([self.d]) self.t.set_dictionary(self.dc) def test_dictionary_update_grows_size1(self): self.d[('S',)] = '1' self.assert_size_call(1) def test_dictionary_update_grows_size4(self): self.d[('S', 'PT', '-Z', 'TOP')] = 'hi' self.assert_size_call(4) def test_dictionary_update_no_grow(self): self.t.set_min_undo_length(4) self.assert_size_call(4) self.clear() self.d[('S', 'T')] = 'nothing' self.assert_size_call(4) def test_dictionary_update_shrink(self): self.d[('S', 'T', 'P', '-Z', '-D')] = '1' self.assert_size_call(5) self.clear() self.d[('A', 'P')] = '2' self.assert_no_size_call() del self.d[('S', 'T', 'P', '-Z', '-D')] self.assert_size_call(2) def test_dictionary_update_no_shrink(self): self.t.set_min_undo_length(7) self.d[('S', 'T', 'P', '-Z', '-D')] = '1' del self.d[('S', 'T', 'P', '-Z', '-D')] self.assert_size_call(7) def test_translation_calls_restrict(self): self.t.translate(stroke('S')) self.assert_size_call(0)
def setUp(self): self.t = Translator() self.s = type(self).FakeState() self.t._state = self.s self.d = StenoDictionary() self.dc = StenoDictionaryCollection() self.dc.set_dicts([self.d]) self.t.set_dictionary(self.dc)
def setUp(self): self.d = StenoDictionary() self.dc = StenoDictionaryCollection() self.dc.set_dicts([self.d]) self.s = _State() self.o = self.CaptureOutput() self.tlor = Translator() self.tlor.set_dictionary(self.dc) self.tlor.add_listener(self.o) self.tlor.set_state(self.s)
def test_dictionary_collection_longest_key(self): k1 = ('S',) k2 = ('S', 'T') k3 = ('S', 'T' , 'R') dc = StenoDictionaryCollection() self.assertEqual(dc.longest_key, 0) d1 = StenoDictionary() d1._path = 'd1' d1.save = lambda: None d1[k1] = 'a' dc.set_dicts([d1]) self.assertEqual(dc.longest_key, 1) d1[k2] = 'a' self.assertEqual(dc.longest_key, 2) d2 = StenoDictionary() d2._path = 'd2' d2[k3] = 'c' dc.set_dicts([d1, d2]) self.assertEqual(dc.longest_key, 3) del d1[k2] self.assertEqual(dc.longest_key, 3) dc.set_dicts([d1]) self.assertEqual(dc.longest_key, 1) dc.set_dicts([]) self.assertEqual(dc.longest_key, 0)
def test_dictionary_collection_writeable(): d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' d2.readonly = True dc = StenoDictionaryCollection([d2, d1]) assert dc.first_writable() == d1 dc.set(('S',), 'A') assert d1[('S',)] == 'A' assert d2[('S',)] == 'c'
def test_dictionary_collection_writeable(self): d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' d2.readonly = True dc = StenoDictionaryCollection([d2, d1]) self.assertEqual(dc.first_writable(), d1) dc.set(('S',), 'A') self.assertEqual(d1[('S',)], 'A') self.assertEqual(d2[('S',)], 'c')
def test_dictionary_collection_writeable(self): d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' d2.readonly = True dc = StenoDictionaryCollection([d2, d1]) self.assertEqual(dc.first_writable(), d1) dc.set(('S', ), 'A') self.assertEqual(d1[('S', )], 'A') self.assertEqual(d2[('S', )], 'c')
def test_dictionary_collection_writeable(): d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' d2.readonly = True dc = StenoDictionaryCollection([d2, d1]) assert dc.first_writable() == d1 dc.set(('S', ), 'A') assert d1[('S', )] == 'A' assert d2[('S', )] == 'c'
def _set_dictionaries(self, dictionaries): def dictionaries_changed(l1, l2): if len(l1) != len(l2): return True for d1, d2 in zip(l1, l2): if d1 is not d2: return True return False if not dictionaries_changed(dictionaries, self._dictionaries.dicts): # No change. return self._dictionaries = StenoDictionaryCollection(dictionaries) self._translator.set_dictionary(self._dictionaries) self._trigger_hook('dictionaries_loaded', self._dictionaries)
def __init__(self): self._undo_length = 0 self._dictionary = None self.set_dictionary(StenoDictionaryCollection()) self._listeners = set() self._state = _State() self._to_undo = [] self._to_do = 0
def setup_method(self): self.stroke_limit = 8 self.t = Translator(self.stroke_limit) self.s = type(self).FakeState() self.t._state = self.s self.d = StenoDictionary() self.dc = StenoDictionaryCollection([self.d]) self.t.set_dictionary(self.dc)
def test_casereverse_lookup(): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('PWAOUFL', )] = 'beautiful' d1[('WAOUFL', )] = 'beAuTIFul' d2 = StenoDictionary() d2[('PW-FL', )] = 'BEAUTIFUL' d3 = StenoDictionary() d3[('WAOUFL', )] = 'not beautiful' dc.set_dicts([d1, d2, d3]) assert dc.casereverse_lookup('beautiful') == { 'beautiful', 'BEAUTIFUL', 'beAuTIFul' }
def __init__(self, stroke_limit=KEY_STROKE_LIMIT): self._stroke_limit = stroke_limit self._undo_length = 0 self._dictionary = None self.set_dictionary(StenoDictionaryCollection()) self._listeners = set() self._state = _State() self._to_undo = [] self._to_do = 0
def test_changing_state(self): output = [] def listener(undo, do, prev): prev = list(prev) if prev else None output.append((undo, do, prev)) d = StenoDictionary() d[('S', 'P')] = 'hi' dc = StenoDictionaryCollection() dc.set_dicts([d]) t = Translator() t.set_dictionary(dc) t.translate(stroke('T')) t.translate(stroke('S')) s = copy.deepcopy(t.get_state()) t.add_listener(listener) expected = [([Translation([stroke('S')], None)], [Translation([stroke('S'), stroke('P')], 'hi')], [Translation([stroke('T')], None)])] t.translate(stroke('P')) self.assertEqual(output, expected) del output[:] t.set_state(s) t.translate(stroke('P')) self.assertEqual(output, expected) del output[:] t.clear_state() t.translate(stroke('P')) self.assertEqual(output, [([], [Translation([stroke('P')], None)], None)]) del output[:] t.set_state(s) t.translate(stroke('P')) self.assertEqual(output, [([], [Translation([stroke('P')], None)], [Translation([stroke('S'), stroke('P')], 'hi')])])
def model_test(monkeypatch, request): state = request.function.__doc__ # Patch configuration directory. current_dir = Path('.').resolve() monkeypatch.setattr('plover.misc.CONFIG_DIR', str(current_dir)) monkeypatch.setattr('plover.gui_qt.dictionaries_widget.CONFIG_DIR', str(current_dir)) # Disable i18n support. monkeypatch.setattr('plover.gui_qt.dictionaries_widget._', lambda s: s) # Fake config. config = mock.PropertyMock() config.return_value = {} # Dictionaries. dictionaries = StenoDictionaryCollection() # Fake engine. engine = mock.MagicMock(spec=''' __enter__ __exit__ config signal_connect '''.split()) engine.__enter__.return_value = engine type(engine).config = config signals = mock.MagicMock() config.return_value = { 'dictionaries': config_dictionaries_from_state(state) if state else [], 'classic_dictionaries_display_order': False, } # Setup model. model = DictionariesModel(engine, {name: name for name in ICON_TO_CHAR}, max_undo=5) for slot in ''' dataChanged layoutAboutToBeChanged layoutChanged has_undo_changed '''.split(): getattr(model, slot).connect(getattr(signals, slot)) connections = dict(call.args for call in engine.signal_connect.mock_calls) assert connections.keys() == {'config_changed', 'dictionaries_loaded'} config.reset_mock() engine.reset_mock() # Test helper. test = ModelTest(config, dictionaries, engine, model, signals, connections, state) if state and any(icon != 'loading' for enabled, icon, path in parse_state(state)): test.load_dictionaries(state) test.reset_mocks() return test
def setUp(self): self.d = StenoDictionary() self.dc = StenoDictionaryCollection() self.dc.set_dicts([self.d]) self.s = _State() self.o = type(self).CaptureOutput()
def test_dictionary_collection(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' dc.set_dicts([d1, d2]) self.assertEqual(dc.lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') f = lambda k, v: v == 'c' dc.add_filter(f) self.assertIsNone(dc.lookup(('S',))) self.assertEqual(dc.raw_lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S',)]) dc.remove_filter(f) self.assertEqual(dc.lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S',)]) dc.set(('S',), 'e') self.assertEqual(dc.lookup(('S',)), 'e') self.assertEqual(d2[('S',)], 'e')
def test_dictionary_collection(): d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d1.path = 'd1' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' d2.path = 'd2' dc = StenoDictionaryCollection([d2, d1]) assert dc.lookup(('S',)) == 'c' assert dc.lookup(('W',)) == 'd' assert dc.lookup(('T',)) == 'b' f = lambda k, v: v == 'c' dc.add_filter(f) assert dc.lookup(('S',)) is None assert dc.raw_lookup(('S',)) == 'c' assert dc.lookup(('W',)) == 'd' assert dc.lookup(('T',)) == 'b' assert dc.reverse_lookup('c') == [('S',)] dc.remove_filter(f) assert dc.lookup(('S',)) == 'c' assert dc.lookup(('W',)) == 'd' assert dc.lookup(('T',)) == 'b' assert dc.reverse_lookup('c') == [('S',)] dc.set(('S',), 'e') assert dc.lookup(('S',)) == 'e' assert d2[('S',)] == 'e' dc.set(('S',), 'f', path='d1') assert dc.lookup(('S',)) == 'e' assert d1[('S',)] == 'f' assert d2[('S',)] == 'e' # Iterating on a StenoDictionaryCollection is # the same as iterating on its dictionaries' paths. assert list(dc) == ['d2', 'd1'] # Test get and []. assert dc.get('d1') == d1 assert dc['d1'] == d1 assert dc.get('invalid') is None with pytest.raises(KeyError): dc['invalid']
def test_reverse_lookup(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('PWAOUFL',)] = 'beautiful' d1[('WAOUFL',)] = 'beautiful' d2 = StenoDictionary() d2[('PW-FL',)] = 'beautiful' d3 = StenoDictionary() d3[('WAOUFL',)] = 'not beautiful' # Simple test. dc.set_dicts([d1]) assertCountEqual(self, dc.reverse_lookup('beautiful'), [('PWAOUFL',), ('WAOUFL',)]) # No duplicates. dc.set_dicts([d2, StenoDictionary(d2)]) assertCountEqual(self, dc.reverse_lookup('beautiful'), [('PW-FL',)]) # Don't stop at the first dictionary with matches. dc.set_dicts([d1, d2]) assertCountEqual(self, dc.reverse_lookup('beautiful'), [('PWAOUFL',), ('WAOUFL',), ('PW-FL',)]) # Ignore keys overriden by a higher precedence dictionary. dc.set_dicts([d1, d2, d3]) assertCountEqual(self, dc.reverse_lookup('beautiful'), [('PWAOUFL',), ('PW-FL',)])
def test_dictionary_collection(): d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d1.path = 'd1' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' d2.path = 'd2' dc = StenoDictionaryCollection([d2, d1]) assert dc.lookup(('S', )) == 'c' assert dc.lookup(('W', )) == 'd' assert dc.lookup(('T', )) == 'b' f = lambda k, v: v == 'c' dc.add_filter(f) assert dc.lookup(('S', )) is None assert dc.raw_lookup(('S', )) == 'c' assert dc.lookup(('W', )) == 'd' assert dc.lookup(('T', )) == 'b' assert dc.reverse_lookup('c') == {('S', )} dc.remove_filter(f) assert dc.lookup(('S', )) == 'c' assert dc.lookup(('W', )) == 'd' assert dc.lookup(('T', )) == 'b' assert dc.reverse_lookup('c') == {('S', )} dc.set(('S', ), 'e') assert dc.lookup(('S', )) == 'e' assert d2[('S', )] == 'e' dc.set(('S', ), 'f', path='d1') assert dc.lookup(('S', )) == 'e' assert d1[('S', )] == 'f' assert d2[('S', )] == 'e' # Iterating on a StenoDictionaryCollection is # the same as iterating on its dictionaries' paths. assert list(dc) == ['d2', 'd1'] # Test get and []. assert dc.get('d1') == d1 assert dc['d1'] == d1 assert dc.get('invalid') is None with pytest.raises(KeyError): dc['invalid']
def test_dictionary_enabled(): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1.path = 'd1' d1[('TEFT', )] = 'test1' d1[('TEFGT', )] = 'Testing' d2 = StenoDictionary() d2[('TEFT', )] = 'test2' d2[('TEFT', '-G')] = 'Testing' d2.path = 'd2' dc.set_dicts([d2, d1]) assert dc.lookup(('TEFT', )) == 'test2' assert dc.raw_lookup(('TEFT', )) == 'test2' assert dc.casereverse_lookup('testing') == ['Testing'] assert dc.reverse_lookup('Testing') == {('TEFT', '-G'), ('TEFGT', )} d2.enabled = False assert dc.lookup(('TEFT', )) == 'test1' assert dc.raw_lookup(('TEFT', )) == 'test1' assert dc.casereverse_lookup('testing') == ['Testing'] assert dc.reverse_lookup('Testing') == {('TEFGT', )} d1.enabled = False assert dc.lookup(('TEST', )) is None assert dc.raw_lookup(('TEFT', )) is None assert dc.casereverse_lookup('testing') == [] assert dc.reverse_lookup('Testing') == set()
def test_dictionary_collection(self): d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d1.path = 'd1' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' d2.path = 'd2' dc = StenoDictionaryCollection([d2, d1]) self.assertEqual(dc.lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') f = lambda k, v: v == 'c' dc.add_filter(f) self.assertIsNone(dc.lookup(('S', ))) self.assertEqual(dc.raw_lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S', )]) dc.remove_filter(f) self.assertEqual(dc.lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S', )]) dc.set(('S', ), 'e') self.assertEqual(dc.lookup(('S', )), 'e') self.assertEqual(d2[('S', )], 'e') dc.set(('S', ), 'f', path='d1') self.assertEqual(dc.lookup(('S', )), 'e') self.assertEqual(d1[('S', )], 'f') self.assertEqual(d2[('S', )], 'e') # Iterating on a StenoDictionaryCollection is # the same as iterating on its dictionaries' paths. self.assertEqual(list(dc), ['d2', 'd1']) # Test get and []. self.assertEqual(dc.get('d1'), d1) self.assertEqual(dc['d1'], d1) self.assertEqual(dc.get('invalid'), None) with self.assertRaises(KeyError): dc['invalid']
def test_dictionary_enabled(): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1.path = 'd1' d1[('TEFT',)] = 'test1' d1[('TEFGT',)] = 'Testing' d2 = StenoDictionary() d2[('TEFT',)] = 'test2' d2[('TEFT', '-G')] = 'Testing' d2.path = 'd2' dc.set_dicts([d2, d1]) assert dc.lookup(('TEFT',)) == 'test2' assert dc.raw_lookup(('TEFT',)) == 'test2' assert dc.casereverse_lookup('testing') == ['Testing'] assert dc.reverse_lookup('Testing') == [('TEFT', '-G'), ('TEFGT',)] d2.enabled = False assert dc.lookup(('TEFT',)) == 'test1' assert dc.raw_lookup(('TEFT',)) == 'test1' assert dc.casereverse_lookup('testing') == ['Testing'] assert dc.reverse_lookup('Testing') == [('TEFGT',)] d1.enabled = False assert dc.lookup(('TEST',)) is None assert dc.raw_lookup(('TEFT',)) is None assert dc.casereverse_lookup('testing') is None assert dc.reverse_lookup('Testing') == []
def test_dictionary_enabled(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1.path = 'd1' d1[('TEFT',)] = 'test1' d1[('TEFGT',)] = 'Testing' d2 = StenoDictionary() d2[('TEFT',)] = 'test2' d2[('TEFT','-G')] = 'Testing' d2.path = 'd2' dc.set_dicts([d2, d1]) self.assertEqual(dc.lookup(('TEFT',)), 'test2') self.assertEqual(dc.raw_lookup(('TEFT',)), 'test2') self.assertEqual(dc.casereverse_lookup('testing'), ['Testing']) assertCountEqual(self, dc.reverse_lookup('Testing'), [('TEFGT',), ('TEFT', '-G')]) d2.enabled = False self.assertEqual(dc.lookup(('TEFT',)), 'test1') self.assertEqual(dc.raw_lookup(('TEFT',)), 'test1') self.assertEqual(dc.casereverse_lookup('testing'), ['Testing']) assertCountEqual(self, dc.reverse_lookup('Testing'), [('TEFGT',)]) d1.enabled = False self.assertEqual(dc.lookup(('TEST',)), None) self.assertEqual(dc.raw_lookup(('TEFT',)), None) self.assertEqual(dc.casereverse_lookup('testing'), None) assertCountEqual(self, dc.reverse_lookup('Testing'), [])
def test_dictionary_collection(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('S', )] = 'a' d1[('T', )] = 'b' d2 = StenoDictionary() d2[('S', )] = 'c' d2[('W', )] = 'd' dc.set_dicts([d1, d2]) self.assertEqual(dc.lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') f = lambda k, v: v == 'c' dc.add_filter(f) self.assertIsNone(dc.lookup(('S', ))) self.assertEqual(dc.raw_lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S', )]) dc.remove_filter(f) self.assertEqual(dc.lookup(('S', )), 'c') self.assertEqual(dc.lookup(('W', )), 'd') self.assertEqual(dc.lookup(('T', )), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S', )]) dc.set(('S', ), 'e') self.assertEqual(dc.lookup(('S', )), 'e') self.assertEqual(d2[('S', )], 'e')
def test_reverse_lookup(): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('PWAOUFL',)] = 'beautiful' d1[('WAOUFL',)] = 'beautiful' d2 = StenoDictionary() d2[('PW-FL',)] = 'beautiful' d3 = StenoDictionary() d3[('WAOUFL',)] = 'not beautiful' # Simple test. dc.set_dicts([d1]) assert dc.reverse_lookup('beautiful') == [('PWAOUFL',), ('WAOUFL',)] # No duplicates. d2_copy = StenoDictionary() d2_copy.update(d2) dc.set_dicts([d2_copy, d2]) assert dc.reverse_lookup('beautiful') == [('PW-FL',)] # Don't stop at the first dictionary with matches. dc.set_dicts([d2, d1]) assert dc.reverse_lookup('beautiful') == [('PW-FL',), ('PWAOUFL',), ('WAOUFL',)] # Ignore keys overridden by a higher precedence dictionary. dc.set_dicts([d3, d2, d1]) assert dc.reverse_lookup('beautiful') == [('PW-FL',), ('PWAOUFL',)]
def test_dictionary_collection(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('S',)] = 'a' d1[('T',)] = 'b' d1.path = 'd1' d2 = StenoDictionary() d2[('S',)] = 'c' d2[('W',)] = 'd' d2.path = 'd2' dc.set_dicts([d2, d1]) self.assertEqual(dc.lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') f = lambda k, v: v == 'c' dc.add_filter(f) self.assertIsNone(dc.lookup(('S',))) self.assertEqual(dc.raw_lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S',)]) dc.remove_filter(f) self.assertEqual(dc.lookup(('S',)), 'c') self.assertEqual(dc.lookup(('W',)), 'd') self.assertEqual(dc.lookup(('T',)), 'b') self.assertEqual(dc.reverse_lookup('c'), [('S',)]) dc.set(('S',), 'e') self.assertEqual(dc.lookup(('S',)), 'e') self.assertEqual(d2[('S',)], 'e') dc.set(('S',), 'f', path='d1') self.assertEqual(dc.lookup(('S',)), 'e') self.assertEqual(d1[('S',)], 'f') self.assertEqual(d2[('S',)], 'e') # Iterating on a StenoDictionaryCollection is # the same as iterating on its dictionaries' paths. self.assertEqual(list(dc), ['d2', 'd1']) # Test get and []. self.assertEqual(dc.get('d1'), d1) self.assertEqual(dc['d1'], d1) self.assertEqual(dc.get('invalid'), None) with self.assertRaises(KeyError): dc['invalid']
def test_translator(): # It's not clear that this test is needed anymore. There are separate # tests for _translate_stroke and test_translate_calls_translate_stroke # makes sure that translate calls it properly. But since I already wrote # this test I'm going to keep it. class Output: def __init__(self): self._output = [] def write(self, undo, do, prev): for t in undo: self._output.pop() for t in do: if t.english: self._output.append(t.english) else: self._output.append('/'.join(t.rtfcre)) def get(self): return ' '.join(self._output) def clear(self): del self._output[:] d = StenoDictionary() out = Output() t = Translator() dc = StenoDictionaryCollection([d]) t.set_dictionary(dc) t.add_listener(out.write) t.translate(stroke('S')) assert out.get() == 'S' t.translate(stroke('T')) assert out.get() == 'S T' t.translate(stroke('*')) assert out.get() == 'S' t.translate(stroke('*')) # Undo buffer ran out assert out.get() == 'S ' + BACK_STRING t.set_min_undo_length(3) out.clear() t.translate(stroke('S')) assert out.get() == 'S' t.translate(stroke('T')) assert out.get() == 'S T' t.translate(stroke('*')) assert out.get() == 'S' t.translate(stroke('*')) assert out.get() == '' out.clear() d[('S', )] = 't1' d[('T', )] = 't2' d[('S', 'T')] = 't3' t.translate(stroke('S')) assert out.get() == 't1' t.translate(stroke('T')) assert out.get() == 't3' t.translate(stroke('T')) assert out.get() == 't3 t2' t.translate(stroke('S')) assert out.get() == 't3 t2 t1' t.translate(stroke('*')) assert out.get() == 't3 t2' t.translate(stroke('*')) assert out.get() == 't3' t.translate(stroke('*')) assert out.get() == 't1' t.translate(stroke('*')) assert out.get() == '' t.translate(stroke('S')) assert out.get() == 't1' t.translate(stroke('T')) assert out.get() == 't3' t.translate(stroke('T')) assert out.get() == 't3 t2' d[('S', 'T', 'T')] = 't4' d[('S', 'T', 'T', 'S')] = 't5' t.translate(stroke('S')) assert out.get() == 't5' t.translate(stroke('*')) assert out.get() == 't3 t2' t.translate(stroke('*')) assert out.get() == 't3' t.translate(stroke('T')) assert out.get() == 't4' t.translate(stroke('S')) assert out.get() == 't5' t.translate(stroke('S')) assert out.get() == 't5 t1' t.translate(stroke('*')) assert out.get() == 't5' t.translate(stroke('*')) assert out.get() == 't4' t.translate(stroke('*')) assert out.get() == 't3' t.translate(stroke('*')) assert out.get() == 't1' t.translate(stroke('*')) assert out.get() == '' d.clear() s = stroke('S') t.translate(s) t.translate(s) t.translate(s) t.translate(s) s = stroke('*') t.translate(s) t.translate(s) t.translate(s) t.translate(s) # Not enough undo to clear output. assert out.get() == 'S ' + BACK_STRING out.clear() t.remove_listener(out.write) t.translate(stroke('S')) assert out.get() == ''
def test_search(): dc = StenoDictionaryCollection() # Similarity is based on string equality after removing case and stripping special characters from the ends. d1 = StenoDictionary() d1[('WAOUFL', )] = 'beautiful' d1[('PWAOUFL', )] = 'Beautiful' d1[('PWAOUT', '-FL')] = '{^BEAUTIFUL} ' d1[('ULG', )] = 'ugly' dc.set_dicts([d1]) assert dc.find_similar('beautiful') == [('Beautiful', {('PWAOUFL', )}), ('beautiful', {('WAOUFL', )}), ('{^BEAUTIFUL} ', {('PWAOUT', '-FL')})] assert dc.find_similar('{#BEAUtiful}{^}') == [ ('Beautiful', {('PWAOUFL', )}), ('beautiful', {('WAOUFL', )}), ('{^BEAUTIFUL} ', {('PWAOUT', '-FL')}) ] # Translations found in multiple dicts should combine non-overlapping keys in the results. d2 = StenoDictionary() del d1[('PWAOUT', '-FL')] d2[('PW-FL', )] = 'beautiful' dc.set_dicts([d1, d2]) assert dc.find_similar('beautiful') == [('Beautiful', {('PWAOUFL', )}), ('beautiful', {('WAOUFL', ), ('PW-FL', )})] # If all possible keys for a translation are overridden, that translation should not be returned. d3 = StenoDictionary() d3[('PW-FL', )] = 'not beautiful' d3[('WAOUFL', )] = 'not beautiful' dc.set_dicts([d3, d1, d2]) assert dc.find_similar('beautiful') == [('Beautiful', {('PWAOUFL', )})] # For partial word search, similar words will be returned first, but if the count is greater than that, # the next words in sorted order which are supersets are returned. Also stops at the end of the dictionary. dc.set_dicts([d1]) d1[('PWAOU', )] = 'beau' d1[('PWAOUFL', 'HREU')] = 'beautifully' d1[('UG', 'HREU', '-PBS')] = 'ugliness' assert dc.find_partial('beau', count=4) == [('beau', {('PWAOU', )}), ('Beautiful', {('PWAOUFL', )}), ('beautiful', {('WAOUFL', )}), ('beautifully', {('PWAOUFL', 'HREU')})] assert dc.find_partial('UGLY', count=2) == [('ugly', {('ULG', )})] # Even if a word isn't present, the search will return words going forward # from the index where it would be found if it was there. assert dc.find_partial('beaut', count=3) == [('Beautiful', {('PWAOUFL', )}), ('beautiful', {('WAOUFL', )}), ('beautifully', {('PWAOUFL', 'HREU')})] # Regex search is straightforward; return up to count entries in order that match the given regular expression. # If no regex metacharacters are present, should just be a case-sensitive starts-with search. assert dc.find_regex('beau', count=4) == [('beau', {('PWAOU', )}), ('beautiful', {('WAOUFL', )}), ('beautifully', {('PWAOUFL', 'HREU')})] assert dc.find_regex('beautiful.?.?', count=2) == [('beautiful', {('WAOUFL', )}), ('beautifully', {('PWAOUFL', 'HREU')})] assert dc.find_regex(' beautiful', count=3) == [] assert dc.find_regex('(b|u).{3}$', count=2) == [('beau', {('PWAOU', )}), ('ugly', {('ULG', )})] assert dc.find_regex('.*ly', count=5) == [('beautifully', {('PWAOUFL', 'HREU')}), ('ugly', {('ULG', )})] # Regex errors won't raise if the algorithm short circuits a pattern with no possible matches. assert dc.find_regex('an open group that doesn\'t raise(', count=5) == [] with pytest.raises(re.error): print(dc.find_regex('beautiful...an open group(', count=1))
def test_reverse_lookup(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1[('PWAOUFL', )] = 'beautiful' d1[('WAOUFL', )] = 'beautiful' d2 = StenoDictionary() d2[('PW-FL', )] = 'beautiful' d3 = StenoDictionary() d3[('WAOUFL', )] = 'not beautiful' # Simple test. dc.set_dicts([d1]) self.assertCountEqual(dc.reverse_lookup('beautiful'), [('PWAOUFL', ), ('WAOUFL', )]) # No duplicates. d2_copy = StenoDictionary() d2_copy.update(d2) dc.set_dicts([d2_copy, d2]) self.assertCountEqual(dc.reverse_lookup('beautiful'), [('PW-FL', )]) # Don't stop at the first dictionary with matches. dc.set_dicts([d2, d1]) self.assertCountEqual(dc.reverse_lookup('beautiful'), [('PWAOUFL', ), ('WAOUFL', ), ('PW-FL', )]) # Ignore keys overridden by a higher precedence dictionary. dc.set_dicts([d3, d2, d1]) self.assertCountEqual(dc.reverse_lookup('beautiful'), [('PWAOUFL', ), ('PW-FL', )])
def test_dictionary_enabled(self): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1.path = 'd1' d1[('TEFT', )] = 'test1' d1[('TEFGT', )] = 'Testing' d2 = StenoDictionary() d2[('TEFT', )] = 'test2' d2[('TEFT', '-G')] = 'Testing' d2.path = 'd2' dc.set_dicts([d2, d1]) self.assertEqual(dc.lookup(('TEFT', )), 'test2') self.assertEqual(dc.raw_lookup(('TEFT', )), 'test2') self.assertEqual(dc.casereverse_lookup('testing'), ['Testing']) self.assertCountEqual(dc.reverse_lookup('Testing'), [('TEFGT', ), ('TEFT', '-G')]) d2.enabled = False self.assertEqual(dc.lookup(('TEFT', )), 'test1') self.assertEqual(dc.raw_lookup(('TEFT', )), 'test1') self.assertEqual(dc.casereverse_lookup('testing'), ['Testing']) self.assertCountEqual(dc.reverse_lookup('Testing'), [('TEFGT', )]) d1.enabled = False self.assertEqual(dc.lookup(('TEST', )), None) self.assertEqual(dc.raw_lookup(('TEFT', )), None) self.assertEqual(dc.casereverse_lookup('testing'), None) self.assertCountEqual(dc.reverse_lookup('Testing'), [])
def test_translator(self): # It's not clear that this test is needed anymore. There are separate # tests for _translate_stroke and test_translate_calls_translate_stroke # makes sure that translate calls it properly. But since I already wrote # this test I'm going to keep it. class Output(object): def __init__(self): self._output = [] def write(self, undo, do, prev): for t in undo: self._output.pop() for t in do: if t.english: self._output.append(t.english) else: self._output.append('/'.join(t.rtfcre)) def get(self): return ' '.join(self._output) def clear(self): del self._output[:] d = StenoDictionary() out = Output() t = Translator() dc = StenoDictionaryCollection() dc.set_dicts([d]) t.set_dictionary(dc) t.add_listener(out.write) t.translate(stroke('S')) self.assertEqual(out.get(), 'S') t.translate(stroke('T')) self.assertEqual(out.get(), 'S T') t.translate(stroke('*')) self.assertEqual(out.get(), 'S') t.translate(stroke('*')) self.assertEqual(out.get(), 'S ' + _back_string()) # Undo buffer ran out. t.set_min_undo_length(3) out.clear() t.translate(stroke('S')) self.assertEqual(out.get(), 'S') t.translate(stroke('T')) self.assertEqual(out.get(), 'S T') t.translate(stroke('*')) self.assertEqual(out.get(), 'S') t.translate(stroke('*')) self.assertEqual(out.get(), '' ) out.clear() d[('S',)] = 't1' d[('T',)] = 't2' d[('S', 'T')] = 't3' t.translate(stroke('S')) self.assertEqual(out.get(), 't1') t.translate(stroke('T')) self.assertEqual(out.get(), 't3') t.translate(stroke('T')) self.assertEqual(out.get(), 't3 t2') t.translate(stroke('S')) self.assertEqual(out.get(), 't3 t2 t1') t.translate(stroke('*')) self.assertEqual(out.get(), 't3 t2') t.translate(stroke('*')) self.assertEqual(out.get(), 't3') t.translate(stroke('*')) self.assertEqual(out.get(), 't1') t.translate(stroke('*')) self.assertEqual(out.get(), '') t.translate(stroke('S')) self.assertEqual(out.get(), 't1') t.translate(stroke('T')) self.assertEqual(out.get(), 't3') t.translate(stroke('T')) self.assertEqual(out.get(), 't3 t2') d[('S', 'T', 'T')] = 't4' d[('S', 'T', 'T', 'S')] = 't5' t.translate(stroke('S')) self.assertEqual(out.get(), 't5') t.translate(stroke('*')) self.assertEqual(out.get(), 't3 t2') t.translate(stroke('*')) self.assertEqual(out.get(), 't3') t.translate(stroke('T')) self.assertEqual(out.get(), 't4') t.translate(stroke('S')) self.assertEqual(out.get(), 't5') t.translate(stroke('S')) self.assertEqual(out.get(), 't5 t1') t.translate(stroke('*')) self.assertEqual(out.get(), 't5') t.translate(stroke('*')) self.assertEqual(out.get(), 't4') t.translate(stroke('*')) self.assertEqual(out.get(), 't3') t.translate(stroke('*')) self.assertEqual(out.get(), 't1') t.translate(stroke('*')) self.assertEqual(out.get(), '') d.clear() s = stroke('S') t.translate(s) t.translate(s) t.translate(s) t.translate(s) s = stroke('*') t.translate(s) t.translate(s) t.translate(s) t.translate(s) self.assertEqual(out.get(), 'S ' + _back_string()) # Not enough undo to clear output. out.clear() t.remove_listener(out.write) t.translate(stroke('S')) self.assertEqual(out.get(), '')
class StenoEngine: HOOKS = ''' stroked translated machine_state_changed output_changed config_changed dictionaries_loaded send_string send_backspaces send_key_combination add_translation focus configure lookup quit '''.split() def __init__(self, config, keyboard_emulation): self._config = config self._is_running = False self._queue = Queue() self._lock = threading.RLock() self._machine = None self._machine_state = None self._machine_params = MachineParams(None, None, None) self._formatter = Formatter() self._formatter.set_output(self) self._formatter.add_listener(self._on_translated) self._translator = Translator() self._translator.add_listener(log.translation) self._translator.add_listener(self._formatter.format) self._dictionaries = self._translator.get_dictionary() self._dictionaries_manager = DictionaryLoadingManager() self._running_state = self._translator.get_state() self._keyboard_emulation = keyboard_emulation self._hooks = {hook: [] for hook in self.HOOKS} self._running_extensions = {} def __enter__(self): self._lock.__enter__() return self def __exit__(self, exc_type, exc_value, traceback): self._lock.__exit__(exc_type, exc_value, traceback) def _in_engine_thread(self): raise NotImplementedError() def _same_thread_hook(self, func, *args, **kwargs): if self._in_engine_thread(): func(*args, **kwargs) else: self._queue.put((func, args, kwargs)) def run(self): while True: func, args, kwargs = self._queue.get() try: with self._lock: if func(*args, **kwargs): break except Exception: log.error('engine %s failed', func.__name__[1:], exc_info=True) def _stop(self): self._stop_extensions(self._running_extensions.keys()) if self._machine is not None: self._machine.stop_capture() self._machine = None def _start(self): self._set_output(self._config['auto_start']) self._update(full=True) def _set_dictionaries(self, dictionaries): def dictionaries_changed(l1, l2): if len(l1) != len(l2): return True for d1, d2 in zip(l1, l2): if d1 is not d2: return True return False if not dictionaries_changed(dictionaries, self._dictionaries.dicts): # No change. return self._dictionaries = StenoDictionaryCollection(dictionaries) self._translator.set_dictionary(self._dictionaries) self._trigger_hook('dictionaries_loaded', self._dictionaries) def _update(self, config_update=None, full=False, reset_machine=False): original_config = self._config.as_dict() # Update configuration. if config_update is not None: self._config.update(**config_update) config = self._config.as_dict() else: config = original_config # Create configuration update. if full: config_update = config else: config_update = { option: value for option, value in config.items() if value != original_config[option] } # Update logging. log.set_stroke_filename(config['log_file_name']) log.enable_stroke_logging(config['enable_stroke_logging']) log.enable_translation_logging(config['enable_translation_logging']) # Update output. self._formatter.set_space_placement(config['space_placement']) self._formatter.start_attached = config['start_attached'] self._formatter.start_capitalized = config['start_capitalized'] self._translator.set_min_undo_length(config['undo_levels']) # Update system. system_name = config['system_name'] if system.NAME != system_name: log.info('loading system: %s', system_name) system.setup(system_name) # Update machine. update_keymap = False start_machine = False machine_params = MachineParams(config['machine_type'], config['machine_specific_options'], config['system_keymap']) # Do not reset if only the keymap changed. if self._machine_params is None or \ self._machine_params.type != machine_params.type or \ self._machine_params.options != machine_params.options: reset_machine = True if reset_machine: if self._machine is not None: self._machine.stop_capture() self._machine = None machine_type = config['machine_type'] machine_options = config['machine_specific_options'] machine_class = registry.get_plugin('machine', machine_type).obj log.info('setting machine: %s', machine_type) self._machine = machine_class(machine_options) self._machine.set_suppression(self._is_running) self._machine.add_state_callback(self._machine_state_callback) self._machine.add_stroke_callback(self._machine_stroke_callback) self._machine_params = machine_params update_keymap = True start_machine = True elif self._machine is not None: update_keymap = 'system_keymap' in config_update if update_keymap: machine_keymap = config['system_keymap'] if machine_keymap is not None: self._machine.set_keymap(machine_keymap) if start_machine: self._machine.start_capture() # Update running extensions. enabled_extensions = config['enabled_extensions'] running_extensions = set(self._running_extensions) self._stop_extensions(running_extensions - enabled_extensions) self._start_extensions(enabled_extensions - running_extensions) # Trigger `config_changed` hook. if config_update: self._trigger_hook('config_changed', config_update) # Update dictionaries. config_dictionaries = OrderedDict( (d.path, d) for d in config['dictionaries']) copy_default_dictionaries(config_dictionaries.keys()) # Start by unloading outdated dictionaries. self._dictionaries_manager.unload_outdated() self._set_dictionaries([ d for d in self._dictionaries.dicts if d.path in config_dictionaries and \ d.path in self._dictionaries_manager ]) # And then (re)load all dictionaries. dictionaries = [] for result in self._dictionaries_manager.load( config_dictionaries.keys()): if isinstance(result, DictionaryLoaderException): d = ErroredDictionary(result.path, result.exception) # Only show an error if it's new. if d != self._dictionaries.get(result.path): log.error('loading dictionary `%s` failed: %s', shorten_path(result.path), str(result.exception)) else: d = result d.enabled = config_dictionaries[d.path].enabled dictionaries.append(d) self._set_dictionaries(dictionaries) def _start_extensions(self, extension_list): for extension_name in extension_list: log.info('starting `%s` extension', extension_name) try: extension = registry.get_plugin('extension', extension_name).obj(self) extension.start() except Exception: log.error('initializing extension `%s` failed', extension_name, exc_info=True) else: self._running_extensions[extension_name] = extension def _stop_extensions(self, extension_list): for extension_name in list(extension_list): log.info('stopping `%s` extension', extension_name) extension = self._running_extensions.pop(extension_name) extension.stop() del extension def _quit(self, code): self._stop() self.code = code self._trigger_hook('quit') return True def _toggle_output(self): self._set_output(not self._is_running) def _set_output(self, enabled): if enabled == self._is_running: return self._is_running = enabled if enabled: self._translator.set_state(self._running_state) else: self._translator.clear_state() if self._machine is not None: self._machine.set_suppression(enabled) self._trigger_hook('output_changed', enabled) def _machine_state_callback(self, machine_state): self._same_thread_hook(self._on_machine_state_changed, machine_state) def _machine_stroke_callback(self, steno_keys): self._same_thread_hook(self._on_stroked, steno_keys) @with_lock def _on_machine_state_changed(self, machine_state): assert machine_state is not None self._machine_state = machine_state machine_type = self._config['machine_type'] self._trigger_hook('machine_state_changed', machine_type, machine_state) def _consume_engine_command(self, command): # The first commands can be used whether plover has output enabled or not. if command == 'RESUME': self._set_output(True) return True elif command == 'TOGGLE': self._toggle_output() return True elif command == 'QUIT': self.quit() return True if not self._is_running: return False # These commands can only be run when plover has output enabled. if command == 'SUSPEND': self._set_output(False) elif command == 'CONFIGURE': self._trigger_hook('configure') elif command == 'FOCUS': self._trigger_hook('focus') elif command == 'ADD_TRANSLATION': self._trigger_hook('add_translation') elif command == 'LOOKUP': self._trigger_hook('lookup') else: command_args = command.split(':', 1) command_fn = registry.get_plugin('command', command_args[0]).obj command_fn(self, command_args[1] if len(command_args) == 2 else '') return False def _on_stroked(self, steno_keys): stroke = Stroke(steno_keys) log.stroke(stroke) self._translator.translate(stroke) self._trigger_hook('stroked', stroke) def _on_translated(self, old, new): if not self._is_running: return self._trigger_hook('translated', old, new) def send_backspaces(self, b): if not self._is_running: return self._keyboard_emulation.send_backspaces(b) self._trigger_hook('send_backspaces', b) def send_string(self, s): if not self._is_running: return self._keyboard_emulation.send_string(s) self._trigger_hook('send_string', s) def send_key_combination(self, c): if not self._is_running: return self._keyboard_emulation.send_key_combination(c) self._trigger_hook('send_key_combination', c) def send_engine_command(self, command): suppress = not self._is_running suppress &= self._consume_engine_command(command) if suppress: self._machine.suppress_last_stroke( self._keyboard_emulation.send_backspaces) def toggle_output(self): self._same_thread_hook(self._toggle_output) def set_output(self, enabled): self._same_thread_hook(self._set_output, enabled) @property @with_lock def machine_state(self): return self._machine_state @property @with_lock def output(self): return self._is_running @output.setter def output(self, enabled): self._same_thread_hook(self._set_output, enabled) @property @with_lock def config(self): return self._config.as_dict() @config.setter def config(self, update): self._same_thread_hook(self._update, config_update=update) @with_lock def __getitem__(self, setting): return self._config[setting] def __setitem__(self, setting, value): self.config = {setting: value} def reset_machine(self): self._same_thread_hook(self._update, reset_machine=True) def load_config(self): try: with open(self._config.target_file, 'rb') as f: self._config.load(f) except Exception: log.error('loading configuration failed, resetting to default', exc_info=True) self._config.clear() return False return True def start(self): self._same_thread_hook(self._start) def quit(self, code=0): # We need to go through the queue, even when already called # from the engine thread so _quit's return code does break # the thread out of its main loop. self._queue.put((self._quit, (code, ), {})) def restart(self): self.quit(-1) def join(self): return self.code @with_lock def lookup(self, translation): return self._dictionaries.lookup('/'.join(translation)) @with_lock def raw_lookup(self, translation): return self._dictionaries.raw_lookup('/'.join(translation)) @with_lock def reverse_lookup(self, translation): matches = self._dictionaries.reverse_lookup(translation) return [] if matches is None else matches @with_lock def casereverse_lookup(self, translation): matches = self._dictionaries.casereverse_lookup(translation) return set() if matches is None else matches @with_lock def add_dictionary_filter(self, dictionary_filter): self._dictionaries.add_filter(dictionary_filter) @with_lock def remove_dictionary_filter(self, dictionary_filter): self._dictionaries.remove_filter(dictionary_filter) @with_lock def get_suggestions(self, translation, **kwargs): return Suggestions(self._dictionaries).find(translation, **kwargs) @property @with_lock def translator_state(self): return self._translator.get_state() @translator_state.setter @with_lock def translator_state(self, state): self._translator.set_state(state) @with_lock def clear_translator_state(self, undo=False): if undo: state = self._translator.get_state() self._formatter.format(state.translations, (), None) self._translator.clear_state() @property @with_lock def starting_stroke_state(self): return StartingStrokeState(self._formatter.start_attached, self._formatter.start_capitalized) @starting_stroke_state.setter @with_lock def starting_stroke_state(self, state): self._formatter.start_attached = state.attach self._formatter.start_capitalized = state.capitalize @with_lock def add_translation(self, strokes, translation, dictionary_path=None): if dictionary_path is None: dictionary_path = self._dictionaries.first_writable().path self._dictionaries.set('/'.join(strokes), translation, path=dictionary_path) self._dictionaries.save(path_list=(dictionary_path, )) @property @with_lock def dictionaries(self): return self._dictionaries # Hooks. def _trigger_hook(self, hook, *args, **kwargs): for callback in self._hooks[hook]: try: callback(*args, **kwargs) except Exception: log.error('hook %r callback %r failed', hook, callback, exc_info=True) @with_lock def hook_connect(self, hook, callback): self._hooks[hook].append(callback) @with_lock def hook_disconnect(self, hook, callback): self._hooks[hook].remove(callback)
class TranslateStrokeTestCase(unittest.TestCase): class CaptureOutput(object): output = namedtuple('output', 'undo do prev') def __init__(self): self.output = [] def __call__(self, undo, new, prev): prev = list(prev) if prev else None self.output = type(self).output(undo, new, prev) def t(self, strokes): """A quick way to make a translation.""" strokes = [stroke(x) for x in strokes.split('/')] key = tuple(s.rtfcre for s in strokes) translation = self.dc.lookup(key) return Translation(strokes, translation) def lt(self, translations): """A quick way to make a list of translations.""" return [self.t(x) for x in translations.split()] def define(self, key, value): key = normalize_steno(key) self.d[key] = value def translate(self, stroke): self.tlor.translate(stroke) def assertTranslations(self, expected): self.assertEqual(self.s.translations, expected) def assertOutput(self, undo, do, prev): self.assertEqual(self.o.output, (undo, do, prev)) def setUp(self): self.d = StenoDictionary() self.dc = StenoDictionaryCollection() self.dc.set_dicts([self.d]) self.s = _State() self.o = self.CaptureOutput() self.tlor = Translator() self.tlor.set_dictionary(self.dc) self.tlor.add_listener(self.o) self.tlor.set_state(self.s) def test_first_stroke(self): self.translate(stroke('-B')) self.assertTranslations(self.lt('-B')) self.assertOutput([], self.lt('-B'), None) def test_second_stroke(self): self.define('S/P', 'spiders') self.s.translations = self.lt('S') self.translate(stroke('-T')) self.assertTranslations(self.lt('S -T')) self.assertOutput([], self.lt('-T'), self.lt('S')) def test_second_stroke_tail(self): self.s.tail = self.t('T/A/I/L') self.translate(stroke('-E')) self.assertTranslations(self.lt('E')) self.assertOutput([], self.lt('E'), self.lt('T/A/I/L')) def test_with_translation_1(self): self.define('S', 'is') self.define('-T', 'that') self.s.translations = self.lt('S') self.tlor.set_min_undo_length(2) self.translate(stroke('-T')) self.assertTranslations(self.lt('S -T')) self.assertOutput([], self.lt('-T'), self.lt('S')) self.assertEqual(self.o.output.do[0].english, 'that') def test_with_translation_2(self): self.define('S', 'is') self.define('-T', 'that') self.s.translations = self.lt('S') self.tlor.set_min_undo_length(1) self.translate(stroke('-T')) self.assertTranslations(self.lt('-T')) self.assertOutput([], self.lt('-T'), self.lt('S')) self.assertEqual(self.o.output.do[0].english, 'that') def test_finish_two_translation(self): self.define('S/T', 'hello') self.s.translations = self.lt('S') self.translate(stroke('T')) self.assertTranslations(self.lt('S/T')) self.assertOutput(self.lt('S'), self.lt('S/T'), None) self.assertEqual(self.o.output.do[0].english, 'hello') self.assertEqual(self.o.output.do[0].replaced, self.lt('S')) def test_finish_three_translation(self): self.define('S/T/-B', 'bye') self.s.translations = self.lt('S T') self.translate(stroke('-B')) self.assertTranslations(self.lt('S/T/-B')) self.assertOutput(self.lt('S T'), self.lt('S/T/-B'), None) self.assertEqual(self.o.output.do[0].english, 'bye') self.assertEqual(self.o.output.do[0].replaced, self.lt('S T')) def test_replace_translation(self): self.define('S/T/-B', 'longer') self.s.translations = self.lt('S/T') self.translate(stroke('-B')) self.assertTranslations(self.lt('S/T/-B')) self.assertOutput(self.lt('S/T'), self.lt('S/T/-B'), None) self.assertEqual(self.o.output.do[0].english, 'longer') self.assertEqual(self.o.output.do[0].replaced, self.lt('S/T')) def test_undo(self): self.s.translations = self.lt('POP') self.translate(stroke('*')) self.assertTranslations([]) self.assertOutput(self.lt('POP'), [], None) def test_empty_undo(self): self.translate(stroke('*')) self.assertTranslations([]) self.assertOutput([], [Translation([Stroke('*')], _back_string())], None) def test_undo_translation(self): self.define('P/P', 'pop') self.translate(stroke('P')) self.translate(stroke('P')) self.translate(stroke('*')) self.assertTranslations(self.lt('P')) self.assertOutput(self.lt('P/P'), self.lt('P'), None) def test_undo_longer_translation(self): self.define('P/P/-D', 'popped') self.translate(stroke('P')) self.translate(stroke('P')) self.translate(stroke('-D')) self.translate(stroke('*')) self.assertTranslations(self.lt('P P')) self.assertOutput(self.lt('P/P/-D'), self.lt('P P'), None) def test_undo_tail(self): self.s.tail = self.t('T/A/I/L') self.translate(stroke('*')) self.assertTranslations([]) self.assertOutput([], [Translation([Stroke('*')], _back_string())], self.lt('T/A/I/L')) def test_suffix_folding(self): self.define('K-L', 'look') self.define('-G', '{^ing}') lt = self.lt('K-LG') lt[0].english = 'look {^ing}' self.translate(stroke('K-LG')) self.assertTranslations(lt) def test_suffix_folding_multi_stroke(self): self.define('E/HR', 'he will') self.define('-S', '{^s}') self.translate(stroke('E')) self.translate(stroke('HR-S')) output = ' '.join(t.english for t in self.s.translations) self.assertEqual(output, 'he will {^s}') def test_suffix_folding_doesnt_interfere(self): self.define('E/HR', 'he will') self.define('-S', '{^s}') self.define('E', 'he') self.define('HR-S', 'also') self.translate(stroke('E')) self.translate(stroke('HR-S')) output = ' '.join(t.english for t in self.s.translations) self.assertEqual(output, 'he also') def test_suffix_folding_no_suffix(self): self.define('K-L', 'look') lt = self.lt('K-LG') self.assertEqual(lt[0].english, None) self.translate(stroke('K-LG')) self.assertTranslations(lt) def test_suffix_folding_no_main(self): self.define('-G', '{^ing}') lt = self.lt('K-LG') self.assertEqual(lt[0].english, None) self.translate(stroke('K-LG')) self.assertTranslations(lt) def test_retrospective_insert_space(self): self.define('T/E/S/T', 'a longer key') self.define('PER', 'perfect') self.define('SWAEUGS', 'situation') self.define('PER/SWAEUGS', 'persuasion') self.define('SP*', '{*?}') self.translate(stroke('PER')) self.translate(stroke('SWAEUGS')) self.translate(stroke('SP*')) lt = self.lt('PER') undo = self.lt('PER/SWAEUGS') undo[0].replaced = lt do = self.lt('SP*') do[0].english = 'perfect situation' do[0].is_retrospective_command = True do[0].replaced = undo self.assertTranslations(do) self.assertOutput(undo, do, None) def test_retrospective_insert_space_undefined(self): # Should work when beginning or ending strokes aren't defined self.define('T/E/S/T', 'a longer key') self.define('STWR/STWR', 'test') self.define('SP*', '{*?}') self.translate(stroke('STWR')) self.translate(stroke('STWR')) self.translate(stroke('SP*')) lt = self.lt('STWR') undo = self.lt('STWR/STWR') undo[0].replaced = lt do = self.lt('SP*') do[0].english = 'STWR STWR' do[0].is_retrospective_command = True do[0].replaced = undo self.assertTranslations(do) self.assertOutput(undo, do, None) def test_retrospective_delete_space(self): self.define('T/E/S/T', 'a longer key') self.define('K', 'kick') self.define('B', 'back') self.define('SP*', '{*!}') self.translate(stroke('K')) self.translate(stroke('B')) self.translate(stroke('SP*')) undo = self.lt('K B') do = self.lt('SP*') do[0].english = 'kick{^~|^}back' do[0].is_retrospective_command = True do[0].replaced = undo self.assertTranslations(do) self.assertOutput(undo, do, None) def test_retrospective_delete_space_with_number(self): self.define('T/E/S/T', 'a longer key') self.define('U', 'user') self.define('SP*', '{*!}') self.translate(stroke('U')) self.translate(stroke('1-')) self.translate(stroke('SP*')) undo = self.lt('U 1-') do = self.lt('SP*') do[0].english = 'user{^~|^}{&1}' do[0].is_retrospective_command = True do[0].replaced = undo self.assertTranslations(do) self.assertOutput(undo, do, None) def test_retrospective_delete_space_with_period(self): self.define('T/E/S/T', 'a longer key') self.define('P-P', '{.}') self.define('SH*', 'zshrc') self.define('SP*', '{*!}') self.translate(stroke('P-P')) self.translate(stroke('SH*')) self.translate(stroke('SP*')) undo = self.lt('P-P SH*') do = self.lt('SP*') do[0].english = '{.}{^~|^}zshrc' do[0].is_retrospective_command = True do[0].replaced = undo self.assertTranslations(do) self.assertOutput(undo, do, None) def test_retrospective_toggle_asterisk(self): self.define('T/E/S/T', 'a longer key') self.define('S', 'see') self.define('S*', 'sea') self.define('A*', '{*}') self.translate(stroke('S')) self.translate(stroke('A*')) undo = self.lt('S') do = self.lt('S*') self.assertTranslations(do) self.assertOutput(undo, do, None) def test_repeat_last_stroke1(self): self.define('T/E/S/T', 'a longer key') self.define('TH', 'this') self.define('R*', '{*+}') self.translate(stroke('TH')) self.translate(stroke('R*')) undo = [] do = self.lt('TH') state = self.lt('TH TH') self.assertTranslations(state) self.assertOutput(undo, do, do) def test_repeat_last_stroke2(self): self.define('T/E/S/T', 'a longer key') self.define('THA', 'that') self.define('R*', '{*+}') self.translate(stroke('THA')) self.translate(stroke('R*')) undo = [] do = self.lt('THA') state = self.lt('THA THA') self.assertTranslations(state) self.assertOutput(undo, do, do)
class StenoEngine(object): HOOKS = ''' stroked translated machine_state_changed output_changed config_changed dictionaries_loaded send_string send_backspaces send_key_combination add_translation focus configure lookup quit '''.split() def __init__(self, config, keyboard_emulation): self._config = config self._is_running = False self._queue = Queue() self._lock = threading.RLock() self._machine = None self._machine_state = None self._machine_params = MachineParams(None, None, None) self._formatter = Formatter() self._formatter.set_output(self) self._formatter.add_listener(self._on_translated) self._translator = Translator() self._translator.add_listener(log.translation) self._translator.add_listener(self._formatter.format) self._dictionaries = self._translator.get_dictionary() self._dictionaries_manager = DictionaryLoadingManager() self._running_state = self._translator.get_state() self._keyboard_emulation = keyboard_emulation self._hooks = { hook: [] for hook in self.HOOKS } self._running_extensions = {} def __enter__(self): self._lock.__enter__() return self def __exit__(self, exc_type, exc_value, traceback): self._lock.__exit__(exc_type, exc_value, traceback) def _in_engine_thread(self): raise NotImplementedError() def _same_thread_hook(self, func, *args, **kwargs): if self._in_engine_thread(): func(*args, **kwargs) else: self._queue.put((func, args, kwargs)) def run(self): while True: func, args, kwargs = self._queue.get() try: with self._lock: if func(*args, **kwargs): break except Exception: log.error('engine %s failed', func.__name__[1:], exc_info=True) def _stop(self): self._stop_extensions(self._running_extensions.keys()) if self._machine is not None: self._machine.stop_capture() self._machine = None def _start(self): self._set_output(self._config.get_auto_start()) self._update(full=True) def _set_dictionaries(self, dictionaries): def dictionaries_changed(l1, l2): if len(l1) != len(l2): return True for d1, d2 in zip(l1, l2): if d1 is not d2: return True return False if not dictionaries_changed(dictionaries, self._dictionaries.dicts): # No change. return self._dictionaries = StenoDictionaryCollection(dictionaries) self._translator.set_dictionary(self._dictionaries) self._trigger_hook('dictionaries_loaded', self._dictionaries) def _update(self, config_update=None, full=False, reset_machine=False): original_config = self._config.as_dict() # Update configuration. if config_update is not None: self._config.update(**config_update) config = self._config.as_dict() else: config = original_config # Create configuration update. if full: config_update = config else: config_update = { option: value for option, value in config.items() if value != original_config[option] } if 'machine_type' in config_update: for opt in ( 'machine_specific_options', 'system_keymap', ): config_update[opt] = config[opt] # Update logging. log.set_stroke_filename(config['log_file_name']) log.enable_stroke_logging(config['enable_stroke_logging']) log.enable_translation_logging(config['enable_translation_logging']) # Update output. self._formatter.set_space_placement(config['space_placement']) self._formatter.start_attached = config['start_attached'] self._formatter.start_capitalized = config['start_capitalized'] self._translator.set_min_undo_length(config['undo_levels']) # Update system. system_name = config['system_name'] if system.NAME != system_name: log.info('loading system: %s', system_name) system.setup(system_name) # Update machine. update_keymap = False start_machine = False machine_params = MachineParams(config['machine_type'], config['machine_specific_options'], config['system_keymap']) # Do not reset if only the keymap changed. if self._machine_params is None or \ self._machine_params.type != machine_params.type or \ self._machine_params.options != machine_params.options: reset_machine = True if reset_machine: if self._machine is not None: self._machine.stop_capture() self._machine = None machine_type = config['machine_type'] machine_options = config['machine_specific_options'] try: machine_class = registry.get_plugin('machine', machine_type).obj except Exception as e: raise InvalidConfigurationError(str(e)) log.info('setting machine: %s', machine_type) self._machine = machine_class(machine_options) self._machine.set_suppression(self._is_running) self._machine.add_state_callback(self._machine_state_callback) self._machine.add_stroke_callback(self._machine_stroke_callback) self._machine_params = machine_params update_keymap = True start_machine = True elif self._machine is not None: update_keymap = 'system_keymap' in config_update if update_keymap: machine_keymap = config['system_keymap'] if machine_keymap is not None: self._machine.set_keymap(machine_keymap) if start_machine: self._machine.start_capture() # Update running extensions. enabled_extensions = config['enabled_extensions'] running_extensions = set(self._running_extensions) self._stop_extensions(running_extensions - enabled_extensions) self._start_extensions(enabled_extensions - running_extensions) # Trigger `config_changed` hook. if config_update: self._trigger_hook('config_changed', config_update) # Update dictionaries. config_dictionaries = OrderedDict( (d.path, d) for d in config['dictionaries'] ) copy_default_dictionaries(config_dictionaries.keys()) # Start by unloading outdated dictionaries. self._dictionaries_manager.unload_outdated() self._set_dictionaries([ d for d in self._dictionaries.dicts if d.path in config_dictionaries and \ d.path in self._dictionaries_manager ]) # And then (re)load all dictionaries. dictionaries = [] for result in self._dictionaries_manager.load(config_dictionaries.keys()): if isinstance(result, DictionaryLoaderException): d = ErroredDictionary(result.path, result.exception) # Only show an error if it's new. if d != self._dictionaries.get(result.path): log.error('loading dictionary `%s` failed: %s', shorten_path(result.path), str(result.exception)) else: d = result d.enabled = config_dictionaries[d.path].enabled dictionaries.append(d) self._set_dictionaries(dictionaries) def _start_extensions(self, extension_list): for extension_name in extension_list: log.info('starting `%s` extension', extension_name) try: extension = registry.get_plugin('extension', extension_name).obj(self) extension.start() except Exception: log.error('initializing extension `%s` failed', extension_name, exc_info=True) else: self._running_extensions[extension_name] = extension def _stop_extensions(self, extension_list): for extension_name in list(extension_list): log.info('stopping `%s` extension', extension_name) extension = self._running_extensions.pop(extension_name) extension.stop() del extension def _quit(self, code): self._stop() self.code = code self._trigger_hook('quit') return True def _toggle_output(self): self._set_output(not self._is_running) def _set_output(self, enabled): if enabled == self._is_running: return self._is_running = enabled if enabled: self._translator.set_state(self._running_state) else: self._translator.clear_state() if self._machine is not None: self._machine.set_suppression(enabled) self._trigger_hook('output_changed', enabled) def _machine_state_callback(self, machine_state): self._same_thread_hook(self._on_machine_state_changed, machine_state) def _machine_stroke_callback(self, steno_keys): self._same_thread_hook(self._on_stroked, steno_keys) @with_lock def _on_machine_state_changed(self, machine_state): assert machine_state is not None self._machine_state = machine_state machine_type = self._config.get_machine_type() self._trigger_hook('machine_state_changed', machine_type, machine_state) def _consume_engine_command(self, command): # The first commands can be used whether plover has output enabled or not. if command == 'RESUME': self._set_output(True) return True elif command == 'TOGGLE': self._toggle_output() return True elif command == 'QUIT': self.quit() return True if not self._is_running: return False # These commands can only be run when plover has output enabled. if command == 'SUSPEND': self._set_output(False) elif command == 'CONFIGURE': self._trigger_hook('configure') elif command == 'FOCUS': self._trigger_hook('focus') elif command == 'ADD_TRANSLATION': self._trigger_hook('add_translation') elif command == 'LOOKUP': self._trigger_hook('lookup') else: command_args = command.split(':', 1) command_fn = registry.get_plugin('command', command_args[0]).obj command_fn(self, command_args[1] if len(command_args) == 2 else '') return False def _on_stroked(self, steno_keys): stroke = Stroke(steno_keys) log.stroke(stroke) self._translator.translate(stroke) self._trigger_hook('stroked', stroke) def _on_translated(self, old, new): if not self._is_running: return self._trigger_hook('translated', old, new) def send_backspaces(self, b): if not self._is_running: return self._keyboard_emulation.send_backspaces(b) self._trigger_hook('send_backspaces', b) def send_string(self, s): if not self._is_running: return self._keyboard_emulation.send_string(s) self._trigger_hook('send_string', s) def send_key_combination(self, c): if not self._is_running: return self._keyboard_emulation.send_key_combination(c) self._trigger_hook('send_key_combination', c) def send_engine_command(self, command): suppress = not self._is_running suppress &= self._consume_engine_command(command) if suppress: self._machine.suppress_last_stroke(self._keyboard_emulation.send_backspaces) def toggle_output(self): self._same_thread_hook(self._toggle_output) def set_output(self, enabled): self._same_thread_hook(self._set_output, enabled) @property @with_lock def machine_state(self): return self._machine_state @property @with_lock def output(self): return self._is_running @output.setter def output(self, enabled): self._same_thread_hook(self._set_output, enabled) @property @with_lock def config(self): return self._config.as_dict() @config.setter def config(self, update): self._same_thread_hook(self._update, config_update=update) def reset_machine(self): self._same_thread_hook(self._update, reset_machine=True) def load_config(self): try: with open(self._config.target_file, 'rb') as f: self._config.load(f) except Exception: log.error('loading configuration failed, reseting to default', exc_info=True) self._config.clear() return False return True def start(self): self._same_thread_hook(self._start) def quit(self, code=0): # We need to go through the queue, even when already called # from the engine thread so _quit's return code does break # the thread out of its main loop. self._queue.put((self._quit, (code,), {})) def restart(self): self.quit(-1) def join(self): return self.code @with_lock def machine_specific_options(self, machine_type): return self._config.get_machine_specific_options(machine_type) @with_lock def system_keymap(self, machine_type, system_name): return self._config.get_system_keymap(machine_type, system_name) @with_lock def lookup(self, translation): return self._dictionaries.lookup(translation) @with_lock def raw_lookup(self, translation): return self._dictionaries.raw_lookup(translation) @with_lock def reverse_lookup(self, translation): matches = self._dictionaries.reverse_lookup(translation) return [] if matches is None else matches @with_lock def casereverse_lookup(self, translation): matches = self._dictionaries.casereverse_lookup(translation) return set() if matches is None else matches @with_lock def add_dictionary_filter(self, dictionary_filter): self._dictionaries.add_filter(dictionary_filter) @with_lock def remove_dictionary_filter(self, dictionary_filter): self._dictionaries.remove_filter(dictionary_filter) @with_lock def get_suggestions(self, translation): return Suggestions(self._dictionaries).find(translation) @property @with_lock def translator_state(self): return self._translator.get_state() @translator_state.setter @with_lock def translator_state(self, state): self._translator.set_state(state) @with_lock def clear_translator_state(self, undo=False): if undo: state = self._translator.get_state() self._formatter.format(state.translations, (), None) self._translator.clear_state() @property @with_lock def starting_stroke_state(self): return StartingStrokeState(self._formatter.start_attached, self._formatter.start_capitalized) @starting_stroke_state.setter @with_lock def starting_stroke_state(self, state): self._formatter.start_attached = state.attach self._formatter.start_capitalized = state.capitalize @with_lock def add_translation(self, strokes, translation, dictionary_path=None): if dictionary_path is None: dictionary_path = self._dictionaries.first_writable().path self._dictionaries.set(strokes, translation, path=dictionary_path) self._dictionaries.save(path_list=(dictionary_path,)) @property @with_lock def dictionaries(self): return self._dictionaries # Hooks. def _trigger_hook(self, hook, *args, **kwargs): for callback in self._hooks[hook]: try: callback(*args, **kwargs) except Exception: log.error('hook %r callback %r failed', hook, callback, exc_info=True) @with_lock def hook_connect(self, hook, callback): self._hooks[hook].append(callback) @with_lock def hook_disconnect(self, hook, callback): self._hooks[hook].remove(callback)
def test_reverse_lookup(): dc = StenoDictionaryCollection() d1 = StenoDictionary() d1['PWAOUFL'] = 'beautiful' d1['WAOUFL'] = 'beautiful' d2 = StenoDictionary() d2['PW-FL'] = 'beautiful' d3 = StenoDictionary() d3['WAOUFL'] = 'not beautiful' # Simple test. dc.set_dicts([d1]) assert dc.reverse_lookup('beautiful') == {'PWAOUFL', 'WAOUFL'} # No duplicates. d2_copy = StenoDictionary() d2_copy.update(d2) dc.set_dicts([d2_copy, d2]) assert dc.reverse_lookup('beautiful') == {'PW-FL'} # Don't stop at the first dictionary with matches. dc.set_dicts([d2, d1]) assert dc.reverse_lookup('beautiful') == {'PW-FL', 'PWAOUFL', 'WAOUFL'} # Ignore keys overridden by a higher precedence dictionary. dc.set_dicts([d3, d2, d1]) assert dc.reverse_lookup('beautiful') == {'PW-FL', 'PWAOUFL'}