def repeat_last_translation(translator: Translator, stroke: Stroke, macro_args: str) -> None: ''' Macro to repeat the last translation(s) in Plover. :param translator: The active Plover translator that is executing the macro. :type translator: plover.translation.Translator :param stroke: The current stroke (what invoked this macro). :type stroke: plover.translation.Stroke :param macro_args: The optional arguments specified to the macro as a comma-delimited string. Piece 1: The number of previous translations to repeat. Default is 1. :type macro_args: str ''' # Get the current state translations = translator.get_state().translations if not translations: return # Process input try: num_to_repeat = int(macro_args.split(DELIM_ARGS)[0]) except: num_to_repeat = 1 # Output the new translations for translation in translations[-num_to_repeat:]: repeated_translation = Translation(translation.strokes, translation.english) translator.translate_translation(repeated_translation)
def retro_everything(translator: Translator, stroke: Stroke, cmdline: str): print("\n\n\nRetro everything invoked with: " + str(stroke) + ", " + cmdline) args = cmdline.split(",") left_char = args[0] right_char = args[1] all_translations = translator.get_state().translations affected_translation_cnt = len( list( itertools.takewhile(lambda x: x.strokes[-1] == stroke, reversed(all_translations)))) # translations that _will_ be affected affected_translations = all_translations[-(affected_translation_cnt + 1):] affected_strokes = flatten([x.strokes for x in affected_translations]) affected_string = " ".join( flatten([ recursively_get_old_english(stroke, t) for t in affected_translations ])) resulting_translation = left_char + affected_string + right_char my_trans = Translation(affected_strokes + [stroke], resulting_translation) my_trans.replaced = affected_translations translator.translate_translation(my_trans)
def setup_method(self): self.t = Translator() 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 translate_log(self, engine, log, edits): formatter = Formatter() formatter.set_output(self) formatter.start_attached = True formatter.start_capitalized = True translator = Translator() translator.set_min_undo_length(100) translator.add_listener(formatter.format) with engine: translator.set_dictionary(engine.dictionaries) for i in range(len(log)): stroke = log[i] self.minimum_pos = len(self.text) translator.translate(stroke) j = i - 1 while j > 0 and log[j].start_pos > self.minimum_pos: log[j].start_pos = self.minimum_pos j = j - 1 stroke.start_pos = self.minimum_pos stroke.end_pos = len(self.text) edit = edits.get(i, None) if not (edit is None): self.text = self.text[:self.minimum_pos] + edit return self.text
class TestTranslatorStateSize: class FakeState(_State): def __init__(self): _State.__init__(self) self.last_restrict_call = None def restrict_size(self, n): self.last_restrict_call = n def _check_size_call(self, size): assert self.s.last_restrict_call == size 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_size(self): limit = self.stroke_limit self.t.set_min_undo_length(limit+5) self._check_size_call(limit+5) self.t.set_min_undo_length(limit+2) self._check_size_call(limit+2) self.t.set_min_undo_length(limit-5) self._check_size_call(limit) self.t.set_min_undo_length(limit-2) self._check_size_call(limit)
def __init__(self, config, controller, keyboard_emulation): self._config = config self._controller = controller 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( Formatter.output_type( self._send_backspaces, self._send_string, self._send_key_combination, self._send_engine_command, )) 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 repeat_last_fragment(translator: Translator, stroke: Stroke, macro_args: str) -> None: ''' Macro to repeat the last fragments(s) in Plover. :param translator: The active Plover translator that is executing the macro. :type translator: plover.translation.Translator :param stroke: The current stroke (what invoked this macro). :type stroke: plover.translation.Stroke :param macro_args: The optional arguments specified to the macro as a comma-delimited string. Piece 1: The number of previous fragments to repeat. Default is 1. :type macro_args: str ''' # Get the current state translations = translator.get_state().translations if not translations: return # Process input try: num_to_repeat = int(macro_args.split(DELIM_ARGS)[0]) except: num_to_repeat = 1 # Output the new translations formatter = RetroFormatter(translations) last_fragments = formatter.last_fragments(num_to_repeat) for fragment in last_fragments: new_translation = Translation([stroke], fragment) translator.translate_translation(new_translation)
def setUp(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.set_min_undo_length(100) self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()])
def setup_method(self): self.d = StenoDictionary() self.dc = StenoDictionaryCollection([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 setUp(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() dictionary = StenoDictionary() dictionary.save = lambda: None self.dictionary.set_dicts([dictionary])
def test_translate_calls_translate_stroke(self): t = Translator() s = stroke('S') def check(stroke, state, dictionary, output): self.assertEqual(stroke, s) self.assertEqual(state, t._state) self.assertEqual(dictionary, t._dictionary) self.assertEqual(output, t._output) with patch('plover.translation._translate_stroke', check) as _translate_stroke: t.translate(s)
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)
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)
class BlackboxTester(object): @classmethod def setup_class(cls): for name in dir(cls): if name.startswith('test_'): setattr(cls, name, replay_doc(getattr(cls, name))) def setup_method(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.set_min_undo_length(100) self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()])
def setUp(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()])
class BlackboxTester: @classmethod def setup_class(cls): for name in dir(cls): if name.startswith('test_'): setattr(cls, name, replay_doc(getattr(cls, name))) def setup_method(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.set_min_undo_length(100) self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()])
class BlackboxTest(unittest.TestCase): def setUp(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()]) def test_bug535(self): self.dictionary.set(('P-P',), '{^.^}') self.dictionary.set(('KR*UR',), '{*($c)}') for steno in ( '1', 'P-P', '2', 'KR*UR', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u' $1.20') def test_special_characters(self): self.dictionary.set(('R-R',), '{^}\n{^}') self.dictionary.set(('TAB',), '\t') for steno in ( 'R-R', 'TAB', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'\n\t')
def blackbox_setup(blackbox): blackbox.output = CaptureOutput() blackbox.formatter = Formatter() blackbox.formatter.set_output(blackbox.output) blackbox.translator = Translator() blackbox.translator.set_min_undo_length(100) blackbox.translator.add_listener(blackbox.formatter.format) blackbox.dictionary = blackbox.translator.get_dictionary() blackbox.dictionary.set_dicts([StenoDictionary()])
def setup_method(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.set_min_undo_length(100) self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()])
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._suggestions = Suggestions(self._dictionaries) self._keyboard_emulation = keyboard_emulation self._hooks = {hook: [] for hook in self.HOOKS}
class BlackboxTest(unittest.TestCase): @classmethod def setUpClass(cls): registry.update() system.setup(DEFAULT_SYSTEM_NAME) def setUp(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() dictionary = StenoDictionary() dictionary.save = lambda: None self.dictionary.set_dicts([dictionary]) def test_basic(self): d = load_dictionary( os.path.join(os.path.dirname(__file__), 'show_stroke.py')) self.assertEqual(d.readonly, True) self.assertEqual(d.longest_key, 2) with self.assertRaises(KeyError): d[('STR', )] self.assertEqual(d.get(('STR', )), None) self.assertEqual(d[('STR*', 'STR')], 'STR') self.assertEqual(d.get(('STR*', 'STR')), 'STR') self.assertEqual(d.reverse_lookup('STR'), ()) self.dictionary.set_dicts([d] + self.dictionary.dicts) self.dictionary.set(normalize_steno('STR'), u'center') for steno in ( 'STR', 'STR*', 'STR', 'STR', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u' center STR center')
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 __init__(self, output, log): self._log = log self._output = output self._config = plover.config.Config() with open(plover.config.CONFIG_FILE) as f: self._config.load(f) keymap = self._config.get_machine_specific_options('NKRO Keyboard')['keymap'] self._mapping = {} for steno_key, key_names in keymap.get().items(): for key in key_names: key = key.lower() if not key in PSEUDOKEY_TO_KEYCODE: continue keycode = PSEUDOKEY_TO_KEYCODE[key] self._mapping[keycode] = steno_key self._dicts = DictionaryManager.load(self._config.get_dictionary_file_names()) self._formatter = Formatter() self._formatter.set_output(self._output) self._translator = Translator() self._translator.add_listener(self._formatter.format) self._translator.get_dictionary().set_dicts(self._dicts) self._translator.set_min_undo_length(NB_PREDIT_STROKES) self.reset(full=True, output=False)
def test_listeners(): output1 = [] def listener1(undo, do, prev): output1.append((undo, do, prev)) output2 = [] def listener2(undo, do, prev): output2.append((undo, do, prev)) t = Translator() s = stroke('S') tr = Translation([s], None) expected_output = [([], [tr], [tr])] t.translate(s) t.add_listener(listener1) t.translate(s) assert output1 == expected_output del output1[:] t.add_listener(listener2) t.translate(s) assert output1 == expected_output assert output2 == expected_output del output1[:] del output2[:] t.add_listener(listener2) t.translate(s) assert output1 == expected_output assert output2 == expected_output del output1[:] del output2[:] t.remove_listener(listener1) t.translate(s) assert output1 == [] assert output2 == expected_output del output1[:] del output2[:] t.remove_listener(listener2) t.translate(s) assert output1 == [] assert output2 == []
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)
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)
def test_changing_state(self): output = [] def listener(undo, do, prev): 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'))])
class StenoEngine(object): HOOKS = ''' stroked translated machine_state_changed output_changed config_changed 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._suggestions = Suggestions(self._dictionaries) self._keyboard_emulation = keyboard_emulation self._hooks = {hook: [] for hook in self.HOOKS} 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): if self._machine is not None: self._machine.stop_capture() self._machine = None def _start(self): self._set_output(self._config.get_auto_start()) copy_default_dictionaries(self._config) self._update(full=True) 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 machine. update_keymap = False start_machine = False machine_params = MachineParams(config['machine_type'], config['machine_specific_options'], config['system_keymap']) if reset_machine or machine_params != self._machine_params: 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 = machine_registry.get(machine_type) except NoSuchMachineException as e: raise InvalidConfigurationError(str(e)) 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 dictionaries. dictionaries_files = config['dictionary_file_names'] dictionaries = self._dictionaries_manager.load(dictionaries_files) self._dictionaries.set_dicts(dictionaries) # Trigger `config_changed` hook. if config_update: self._trigger_hook('config_changed', config_update) def _quit(self): self._stop() 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._trigger_hook('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') 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 @with_lock 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): self._same_thread_hook(self._quit) @property @with_lock def machines(self): return sorted(machine_registry.get_all_names()) @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): return self._config.get_system_keymap(machine_type) @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 self._suggestions.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=None): if dictionary is None: dictionary = self._dictionaries.dicts[0].get_path() self._dictionaries.set(strokes, translation, dictionary=dictionary) self._dictionaries.save(path_list=(dictionary, )) @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_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() == ''
class TestTranslateStroke: class CaptureOutput: 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, steno): self.tlor.translate(stroke(steno)) def _check_translations(self, expected): # Hide from traceback on assertions (reduce output size for failed tests). __tracebackhide__ = operator.methodcaller('errisinstance', AssertionError) msg = ''' translations: results: %s expected: %s ''' % (self.s.translations, expected) assert self.s.translations == expected, msg def _check_output(self, undo, do, prev): # Hide from traceback on assertions (reduce output size for failed tests). __tracebackhide__ = operator.methodcaller('errisinstance', AssertionError) msg = ''' output: results: -%s +%s [%s] expected: -%s +%s [%s] ''' % (self.o.output + (undo, do, prev)) assert self.o.output == (undo, do, prev), msg def setup_method(self): self.d = StenoDictionary() self.dc = StenoDictionaryCollection([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('-B') self._check_translations(self.lt('-B')) self._check_output([], self.lt('-B'), None) def test_second_stroke(self): self.define('S/P', 'spiders') self.s.translations = self.lt('S') self.translate('-T') self._check_translations(self.lt('S -T')) self._check_output([], self.lt('-T'), self.lt('S')) def test_second_stroke_tail(self): self.s.tail = self.t('T/A/EU/L') self.translate('-E') self._check_translations(self.lt('E')) self._check_output([], self.lt('E'), self.lt('T/A/EU/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('-T') self._check_translations(self.lt('S -T')) self._check_output([], self.lt('-T'), self.lt('S')) assert 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('-T') self._check_translations(self.lt('-T')) self._check_output([], self.lt('-T'), self.lt('S')) assert 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('T') self._check_translations(self.lt('S/T')) self._check_output(self.lt('S'), self.lt('S/T'), None) assert self.o.output.do[0].english == 'hello' assert 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('-B') self._check_translations(self.lt('S/T/-B')) self._check_output(self.lt('S T'), self.lt('S/T/-B'), None) assert self.o.output.do[0].english == 'bye' assert 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('-B') self._check_translations(self.lt('S/T/-B')) self._check_output(self.lt('S/T'), self.lt('S/T/-B'), None) assert self.o.output.do[0].english == 'longer' assert self.o.output.do[0].replaced == self.lt('S/T') def test_undo(self): self.s.translations = self.lt('POP') self.translate('*') self._check_translations([]) self._check_output(self.lt('POP'), [], None) def test_empty_undo(self): self.translate('*') self._check_translations([]) self._check_output([], [Translation([Stroke('*')], BACK_STRING)], None) def test_undo_translation(self): self.define('P/P', 'pop') self.translate('P') self.translate('P') self.translate('*') self._check_translations(self.lt('P')) self._check_output(self.lt('P/P'), self.lt('P'), None) def test_undo_longer_translation(self): self.define('P/P/-D', 'popped') self.translate('P') self.translate('P') self.translate('-D') self.translate('*') self._check_translations(self.lt('P P')) self._check_output(self.lt('P/P/-D'), self.lt('P P'), None) def test_undo_tail(self): self.s.tail = self.t('T/A/EU/L') self.translate('*') self._check_translations([]) self._check_output([], [Translation([Stroke('*')], BACK_STRING)], [self.s.tail]) 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('K-LG') self._check_translations(lt) def test_suffix_folding_multi_stroke(self): self.define('E/HR', 'he will') self.define('-S', '{^s}') self.translate('E') self.translate('HR-S') output = ' '.join(t.english for t in self.s.translations) assert 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('E') self.translate('HR-S') output = ' '.join(t.english for t in self.s.translations) assert output == 'he also' def test_suffix_folding_no_suffix(self): self.define('K-L', 'look') lt = self.lt('K-LG') assert lt[0].english is None self.translate('K-LG') self._check_translations(lt) def test_suffix_folding_no_main(self): self.define('-G', '{^ing}') lt = self.lt('K-LG') assert lt[0].english is None self.translate('K-LG') self._check_translations(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('PER') self.translate('SWAEUGS') self.translate('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._check_translations(do) self._check_output(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('STWR') self.translate('STWR') self.translate('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._check_translations(do) self._check_output(undo, do, None) def test_retrospective_delete_space(self): self.define('T/E/S/T', 'a longer key') self.define('K', 'kick') self.define('PW', 'back') self.define('SP*', '{*!}') self.translate('K') self.translate('PW') self.translate('SP*') undo = self.lt('K PW') do = self.lt('SP*') do[0].english = 'kick{^~|^}back' do[0].is_retrospective_command = True do[0].replaced = undo self._check_translations(do) self._check_output(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('U') self.translate('1-') self.translate('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._check_translations(do) self._check_output(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('P-P') self.translate('SH*') self.translate('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._check_translations(do) self._check_output(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('S') self.translate('A*') self._check_translations(self.lt('S*')) self._check_output(self.lt('S'), self.lt('S*'), None) def test_retrospective_toggle_empty(self): self.define('A*', '{*}') self.translate('A*') self._check_translations(self.lt('')) assert self.o.output == [] def test_retrospective_toggle_asterisk_replaced1(self): self.define('P-P', '{.}') self.define('SKEL', 'cancel') self.define('SKEL/TO-PB', 'skeleton') self.define('SKEL/TO*PB', 'not skeleton!') self.define('A*', '{*}') self.translate('P-P') self.translate('SKEL') self.translate('TO-PB') self.translate('A*') self._check_translations(self.lt('SKEL/TO*PB')) self._check_output(self.lt('SKEL/TO-PB'), self.lt('SKEL/TO*PB'), self.lt('P-P')) def test_retrospective_toggle_asterisk_replaced2(self): self.define('P-P', '{.}') self.define('SKEL', 'cancel') self.define('SKEL/TO-PB', 'skeleton') self.define('TO*PB', '{^ton}') self.define('A*', '{*}') self.translate('P-P') self.translate('SKEL') self.translate('TO-PB') self.translate('A*') self._check_translations(self.lt('SKEL TO*PB')) self._check_output(self.lt('SKEL/TO-PB'), self.lt('SKEL TO*PB'), self.lt('P-P')) def test_repeat_last_stroke1(self): self.define('T/E/S/T', 'a longer key') self.define('TH', 'this') self.define('R*', '{*+}') self.translate('TH') self.translate('R*') undo = [] do = self.lt('TH') state = self.lt('TH TH') self._check_translations(state) self._check_output(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('THA') self.translate('R*') undo = [] do = self.lt('THA') state = self.lt('THA THA') self._check_translations(state) self._check_output(undo, do, do) def test_untranslate_translation(self): self.tlor.set_min_undo_length(2) self.define('TH', 'this') self.define('THA', 'that') self.translate('TH') self.translate('THA') self.tlor.untranslate_translation(self.t('THA')) self.tlor.untranslate_translation(self.t('TH')) self.tlor.flush() self._check_output(self.lt('TH THA'), [], None)
def test_changing_state(): 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([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')) assert output == expected del output[:] t.set_state(s) t.translate(stroke('P')) assert output == expected del output[:] t.clear_state() t.translate(stroke('P')) assert output == [([], [Translation([stroke('P')], None)], None)] del output[:] t.set_state(s) t.translate(stroke('P')) assert output == [([], [Translation([stroke('P')], None)], [Translation([stroke('S'), stroke('P')], 'hi')])]
class BlackboxTest(unittest.TestCase): def setUp(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.set_min_undo_length(100) self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()]) def test_translator_state_handling(self): # Check if the translator curtailing the list of last translations # according to its dictionary longest key does no affect things # like the restrospective repeate-last-stroke command. self.dictionary.set(('TEFT',), 'test') self.dictionary.set(('R*S',), '{*+}') # Note: the implementation of repeat-last-stroke looks at the last # stroke keys, so we can't use the same trick as for other tests. for keys in ( ('T-', '-E', '-F', '-T'), ('R-', '*', '-S'), ): stroke = Stroke(keys) self.translator.translate(stroke) self.assertEqual(self.output.text, u' test test') def test_bug471(self): # Repeat-last-stroke after typing two numbers outputs the numbers # reversed for some combos. self.dictionary.set(('R*S',), '{*+}') # Note: the implementation of repeat-last-stroke looks at the last # stroke keys, so we can't use the same trick as for other tests. for keys in ( ('#', 'S-', 'T-'), # 12 ('R-', '*', '-S'), ): stroke = Stroke(keys) self.translator.translate(stroke) self.assertEqual(self.output.text, u' 1212') def test_bug535(self): # Currency formatting a number with a decimal fails by not erasing # the previous output. self.dictionary.set(('P-P',), '{^.^}') self.dictionary.set(('KR*UR',), '{*($c)}') for steno in ( '1', 'P-P', '2', 'KR*UR', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u' $1.20') @unittest.expectedFailure def test_bug557(self): # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". for steno, translation in ( ('EU' , 'I' ), ('HRAOEUBG', 'like' ), ('T*' , '{>}{&t}'), ('A*' , '{>}{&a}'), ('KR*' , '{>}{&c}'), ('O*' , '{>}{&o}'), ('S*' , '{>}{&s}'), ): self.dictionary.set(normalize_steno(steno), translation) self.formatter.set_space_placement('After Output') for steno in ( 'EU', 'HRAOEUBG', 'T*', 'A*', 'KR*', 'O*', 'S*', '*', '*', '*', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'I like ta ') def test_special_characters(self): self.dictionary.set(('R-R',), '{^}\n{^}') self.dictionary.set(('TAB',), '\t') for steno in ( 'R-R', 'TAB', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'\n\t')
class BlackboxTest(unittest.TestCase): def setUp(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.set_min_undo_length(100) self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()]) def test_translator_state_handling(self): # Check if the translator curtailing the list of last translations # according to its dictionary longest key does no affect things # like the restrospective repeate-last-stroke command. self.dictionary.set(('TEFT', ), 'test') self.dictionary.set(('R*S', ), '{*+}') # Note: the implementation of repeat-last-stroke looks at the last # stroke keys, so we can't use the same trick as for other tests. for keys in ( ('T-', '-E', '-F', '-T'), ('R-', '*', '-S'), ): stroke = Stroke(keys) self.translator.translate(stroke) self.assertEqual(self.output.text, u' test test') def test_force_lowercase_title(self): self.dictionary.set(('T-LT', ), '{MODE:TITLE}') self.dictionary.set(('TEFT', ), '{>}test') for keys in ( ('T-', '-L', '-T'), ('T-', '-E', '-F', '-T'), ): stroke = Stroke(keys) self.translator.translate(stroke) self.assertEqual(self.output.text, u' test') def test_bug471(self): # Repeat-last-stroke after typing two numbers outputs the numbers # reversed for some combos. self.dictionary.set(('R*S', ), '{*+}') # Note: the implementation of repeat-last-stroke looks at the last # stroke keys, so we can't use the same trick as for other tests. for keys in ( ('#', 'S-', 'T-'), # 12 ('R-', '*', '-S'), ): stroke = Stroke(keys) self.translator.translate(stroke) self.assertEqual(self.output.text, u' 1212') def test_bug535(self): # Currency formatting a number with a decimal fails by not erasing # the previous output. self.dictionary.set(('P-P', ), '{^.^}') self.dictionary.set(('KR*UR', ), '{*($c)}') for steno in ( '1', 'P-P', '2', 'KR*UR', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u' $1.20') def test_bug606(self): for steno, translation in ( ('KWEGS', 'question'), ('-S', '{^s}'), ('TP-PL', '{.}'), ): self.dictionary.set(normalize_steno(steno), translation) self.formatter.set_space_placement('After Output') for steno in ( 'KWEGS', '-S', 'TP-PL', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'questions. ') def test_bug535_spaces_after(self): # Currency formatting a number with a decimal fails by not erasing # the previous output. self.formatter.set_space_placement('After Output') self.dictionary.set(('P-P', ), '{^.^}') self.dictionary.set(('KR*UR', ), '{*($c)}') for steno in ( '1', 'P-P', '2', 'KR*UR', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'$1.20 ') def test_bug557(self): # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". for steno, translation in ( ('EU', 'I'), ('HRAOEUBG', 'like'), ('T*', '{>}{&t}'), ('A*', '{>}{&a}'), ('KR*', '{>}{&c}'), ('O*', '{>}{&o}'), ('S*', '{>}{&s}'), ): self.dictionary.set(normalize_steno(steno), translation) self.formatter.set_space_placement('After Output') for steno in ( 'EU', 'HRAOEUBG', 'T*', 'A*', 'KR*', 'O*', 'S*', '*', '*', '*', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'I like ta ') def test_bug557_resumed(self): # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". for steno, translation in ( ('EU', 'I'), ('HRAOEUBG', 'like'), ('T*', '{>}{&t}'), ('A*', '{>}{&a}'), ('KR*', '{>}{&c}'), ('O*', '{>}{&o}'), ('S*', '{>}{&s}'), ): self.dictionary.set(normalize_steno(steno), translation) self.formatter.set_space_placement('After Output') for steno in ( 'EU', 'HRAOEUBG', 'T*', 'A*', 'KR*', 'O*', 'S*', '*', '*', '*', '*', '*', 'HRAOEUBG', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'I like like ') def test_bug557_capitalized(self): # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". for steno, translation in ( ('EU', 'I'), ('HRAOEUBG', 'like'), ('T*', '{-|}{&t}'), ('A*', '{-|}{&a}'), ('KR*', '{-|}{&c}'), ('O*', '{-|}{&o}'), ('S*', '{-|}{&s}'), ): self.dictionary.set(normalize_steno(steno), translation) self.formatter.set_space_placement('After Output') for steno in ( 'EU', 'HRAOEUBG', 'T*', 'A*', 'KR*', 'O*', 'S*', '*', '*', '*', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'I like TA ') def test_capitalized_fingerspelling_spaces_after(self): # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". for steno, translation in ( ('HRAOEUBG', 'like'), ('T*', '{&T}'), ('A*', '{&A}'), ('KR*', '{&C}'), ('O*', '{&O}'), ('S*', '{&S}'), ): self.dictionary.set(normalize_steno(steno), translation) self.formatter.set_space_placement('After Output') for steno in ( 'HRAOEUBG', 'T*', 'A*', 'KR*', 'O*', 'S*', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'like TACOS ') def test_special_characters(self): self.dictionary.set(('R-R', ), '{^}\n{^}') self.dictionary.set(('TAB', ), '\t') for steno in ( 'R-R', 'TAB', ): stroke = steno_to_stroke(steno) self.translator.translate(stroke) self.assertEqual(self.output.text, u'\n\t')
def test_listeners(self): output1 = [] def listener1(undo, do, prev): output1.append((undo, do, prev)) output2 = [] def listener2(undo, do, prev): output2.append((undo, do, prev)) t = Translator() s = stroke('S') tr = Translation([s], None) expected_output = [([], [tr], [tr])] t.translate(s) t.add_listener(listener1) t.translate(s) self.assertEqual(output1, expected_output) del output1[:] t.add_listener(listener2) t.translate(s) self.assertEqual(output1, expected_output) self.assertEqual(output2, expected_output) del output1[:] del output2[:] t.add_listener(listener2) t.translate(s) self.assertEqual(output1, expected_output) self.assertEqual(output2, expected_output) del output1[:] del output2[:] t.remove_listener(listener1) t.translate(s) self.assertEqual(output1, []) self.assertEqual(output2, expected_output) del output1[:] del output2[:] t.remove_listener(listener2) t.translate(s) self.assertEqual(output1, []) self.assertEqual(output2, [])
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() == ''
class StenoEngine(object): HOOKS = ''' stroked translated machine_state_changed output_changed config_changed 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._suggestions = Suggestions(self._dictionaries) self._keyboard_emulation = keyboard_emulation self._hooks = { hook: [] for hook in self.HOOKS } 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): if self._machine is not None: self._machine.stop_capture() self._machine = None def _start(self): self._set_output(self._config.get_auto_start()) copy_default_dictionaries(self._config) self._update(full=True) 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 machine. update_keymap = False start_machine = False machine_params = MachineParams(config['machine_type'], config['machine_specific_options'], config['system_keymap']) if reset_machine or machine_params != self._machine_params: 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 = machine_registry.get(machine_type) except NoSuchMachineException as e: raise InvalidConfigurationError(str(e)) 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 dictionaries. dictionaries_files = config['dictionary_file_names'] dictionaries = self._dictionaries_manager.load(dictionaries_files) self._dictionaries.set_dicts(dictionaries) # Trigger `config_changed` hook. if config_update: self._trigger_hook('config_changed', config_update) def _quit(self): self._stop() 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._trigger_hook('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') 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 @with_lock 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): self._same_thread_hook(self._quit) @property @with_lock def machines(self): return sorted(machine_registry.get_all_names()) @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): return self._config.get_system_keymap(machine_type) @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 self._suggestions.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=None): if dictionary is None: dictionary = self._dictionaries.dicts[0].get_path() self._dictionaries.set(strokes, translation, dictionary=dictionary) self._dictionaries.save(path_list=(dictionary,)) @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 Steno: def __init__(self, output, log): self._log = log self._output = output self._config = plover.config.Config() with open(plover.config.CONFIG_FILE) as f: self._config.load(f) keymap = self._config.get_machine_specific_options('NKRO Keyboard')['keymap'] self._mapping = {} for steno_key, key_names in keymap.get().items(): for key in key_names: key = key.lower() if not key in PSEUDOKEY_TO_KEYCODE: continue keycode = PSEUDOKEY_TO_KEYCODE[key] self._mapping[keycode] = steno_key self._dicts = DictionaryManager.load(self._config.get_dictionary_file_names()) self._formatter = Formatter() self._formatter.set_output(self._output) self._translator = Translator() self._translator.add_listener(self._formatter.format) self._translator.get_dictionary().set_dicts(self._dicts) self._translator.set_min_undo_length(NB_PREDIT_STROKES) self.reset(full=True, output=False) def flush(self): self._output.flush() self.reset() def stroke(self, stroke): self._log.debug('stroke(%s)' % stroke.rtfcre) self._output.stroke_start() self._translator.translate(stroke) self._output.stroke_end() def reset(self, full=False, output=True): self._log.debug('reset steno state (full=%s)' % full) state = _State() state.tail = self._translator.get_state().last() if full or state.tail is None: state.tail = Translation([Stroke('*')], None) state.tail.formatting = [_Action(attach=True)] self._translator.set_state(state) if output: self._output.reset() def translate_keycode_to_steno(self, keycode): return self._mapping.get(keycode, None)
class BlackboxTest(unittest.TestCase): def setUp(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.set_min_undo_length(100) self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()]) @simple_replay def test_translator_state_handling(self): # Check if the translator curtailing the list of last translations # according to its dictionary longest key does no affect things # like the restrospective repeate-last-stroke command. r''' "TEFT": "test", "R*S": "{*+}", TEFT/R*S " test test" ''' @simple_replay def test_force_lowercase_title(self): r''' "T-LT": "{MODE:TITLE}", "TEFT": "{>}test", T-LT/TEFT " test" ''' @simple_replay def test_bug471(self): # Repeat-last-stroke after typing two numbers outputs the numbers # reversed for some combos. r''' "R*S": "{*+}", 12/R*S " 1212" ''' @simple_replay def test_bug535(self): # Currency formatting a number with a decimal fails by not erasing # the previous output. r''' "P-P": "{^.^}", "KR*UR": "{*($c)}", 1/P-P/2/KR*UR " $1.20" ''' @simple_replay @spaces_after def test_bug606(self): r''' "KWEGS": "question", "-S": "{^s}", "TP-PL": "{.}", KWEGS/-S/TP-PL "questions. " ''' @simple_replay @spaces_after def test_bug535_spaces_after(self): # Currency formatting a number with a decimal fails by not erasing # the previous output. r''' "P-P": "{^.^}", "KR*UR": "{*($c)}", 1/P-P/2/KR*UR "$1.20 " ''' @simple_replay @spaces_after def test_bug557(self): # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". r''' "EU": "I", "HRAOEUBG": "like", "T*": "{>}{&t}", "A*": "{>}{&a}", "KR*": "{>}{&c}", "O*": "{>}{&o}", "S*": "{>}{&s}", EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/* "I like ta " ''' @simple_replay @spaces_after def test_bug557_resumed(self): # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". r''' "EU": "I", "HRAOEUBG": "like", "T*": "{>}{&t}", "A*": "{>}{&a}", "KR*": "{>}{&c}", "O*": "{>}{&o}", "S*": "{>}{&s}", EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*/*/*/HRAOEUBG "I like like " ''' @simple_replay @spaces_after def test_bug557_capitalized(self): # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". r''' "EU": "I", "HRAOEUBG": "like", "T*": "{-|}{&t}", "A*": "{-|}{&a}", "KR*": "{-|}{&c}", "O*": "{-|}{&o}", "S*": "{-|}{&s}", EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/* "I like TA " ''' @simple_replay @spaces_after def test_capitalized_fingerspelling_spaces_after(self): # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". r''' "HRAOEUBG": "like", "T*": "{&T}", "A*": "{&A}", "KR*": "{&C}", "O*": "{&O}", "S*": "{&S}", HRAOEUBG/T*/A*/KR*/O*/S* "like TACOS " ''' @simple_replay def test_special_characters(self): r''' "R-R": "{^}\n{^}", "TAB": "\t", R-R/TAB "\n\t" ''' @simple_replay def test_automatic_suffix_keys_1(self): r''' "RAEUS": "race", "RAEUZ": "raise", "-S": "{^s}", "-Z": "{^s}", RAEUSZ " races" ''' @simple_replay @spaces_after def test_bug719(self): # Glue (&) does not work with "Spaces After". r''' "P*": "{&P}" P*/P*/P*/P*/P*/P* 'PPPPPP ' ''' @unittest.expectedFailure @simple_replay def test_bug741(self): # Uppercase last word also uppercases next word's prefix. r'''
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 TestTranslatorStateSize: class FakeState(_State): def __init__(self): _State.__init__(self) self.restrict_calls = [] def restrict_size(self, n): self.restrict_calls.append(n) def _check_size_call(self, size): assert self.s.restrict_calls[-1] == size def _check_no_size_call(self): assert self.s.restrict_calls == [] def clear(self): self.s.restrict_calls = [] def setup_method(self): self.t = Translator() self.s = type(self).FakeState() self.t._state = self.s self.d = StenoDictionary() self.dc = StenoDictionaryCollection([self.d]) self.t.set_dictionary(self.dc) @pytest.mark.parametrize('key', ( ('S',), ('S', 'PT', '-Z', 'TOP'), )) def test_dictionary_update_grows_size(self, key): self.d[key] = 'key' self.t.translate(stroke('T-')) self._check_size_call(len(key)) def test_dictionary_update_no_grow(self): self.t.set_min_undo_length(4) self._check_size_call(4) self.clear() self.d[('S', 'T')] = 'nothing' self.t.translate(stroke('T-')) self._check_size_call(4) def test_dictionary_update_shrink(self): self.d[('S', 'T', 'P', '-Z', '-D')] = '1' self.t.translate(stroke('T-')) self._check_size_call(5) self.clear() self.d[('A', 'P')] = '2' self.t.translate(stroke('T-')) self._check_size_call(5) self.clear() del self.d[('S', 'T', 'P', '-Z', '-D')] self.t.translate(stroke('T-')) self._check_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._check_size_call(7) def test_translation_calls_restrict(self): self.t.translate(stroke('S')) self._check_size_call(0)
class TestBlackboxReplays(object): @classmethod def setup_class(cls): for name in dir(cls): if name.startswith('test_'): setattr(cls, name, replay_doc(getattr(cls, name))) def setup_method(self): self.output = CaptureOutput() self.formatter = Formatter() self.formatter.set_output(self.output) self.translator = Translator() self.translator.set_min_undo_length(100) self.translator.add_listener(self.formatter.format) self.dictionary = self.translator.get_dictionary() self.dictionary.set_dicts([StenoDictionary()]) def test_translator_state_handling(self): r''' # Check if the translator curtailing the list of last translations # according to its dictionary longest key does no affect things # like the restrospective repeate-last-stroke command. "TEFT": "test", "R*S": "{*+}", TEFT/R*S " test test" ''' def test_force_lowercase_title(self): r''' "T-LT": "{MODE:TITLE}", "TEFT": "{>}test", T-LT/TEFT " test" ''' def test_bug471(self): r''' # Repeat-last-stroke after typing two numbers outputs the numbers # reversed for some combos. "R*S": "{*+}", 12/R*S " 1212" ''' def test_bug535(self): r''' # Currency formatting a number with a decimal fails by not erasing # the previous output. "P-P": "{^.^}", "KR*UR": "{*($c)}", 1/P-P/2/KR*UR " $1.20" ''' def test_bug606(self): r''' "KWEGS": "question", "-S": "{^s}", "TP-PL": "{.}", :spaces_after KWEGS/-S/TP-PL "questions. " ''' def test_bug535_spaces_after(self): r''' # Currency formatting a number with a decimal fails by not erasing # the previous output. "P-P": "{^.^}", "KR*UR": "{*($c)}", :spaces_after 1/P-P/2/KR*UR "$1.20 " ''' def test_bug557(self): r''' # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". "EU": "I", "HRAOEUBG": "like", "T*": "{>}{&t}", "A*": "{>}{&a}", "KR*": "{>}{&c}", "O*": "{>}{&o}", "S*": "{>}{&s}", :spaces_after EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/* "I like ta " ''' def test_bug557_resumed(self): r''' # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". "EU": "I", "HRAOEUBG": "like", "T*": "{>}{&t}", "A*": "{>}{&a}", "KR*": "{>}{&c}", "O*": "{>}{&o}", "S*": "{>}{&s}", :spaces_after EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/*/*/*/HRAOEUBG "I like like " ''' def test_bug557_capitalized(self): r''' # Using the asterisk key to delete letters in fingerspelled words # occasionally causes problems when the space placement is set to # "After Output". "EU": "I", "HRAOEUBG": "like", "T*": "{-|}{&t}", "A*": "{-|}{&a}", "KR*": "{-|}{&c}", "O*": "{-|}{&o}", "S*": "{-|}{&s}", :spaces_after EU/HRAOEUBG/T*/A*/KR*/O*/S*/*/*/* "I like TA " ''' def test_capitalized_fingerspelling_spaces_after(self): r''' "HRAOEUBG": "like", "T*": "{&T}", "A*": "{&A}", "KR*": "{&C}", "O*": "{&O}", "S*": "{&S}", :spaces_after HRAOEUBG/T*/A*/KR*/O*/S* "like TACOS " ''' def test_special_characters(self): r''' "R-R": "{^}\n{^}", "TAB": "\t", R-R/TAB "\n\t" ''' def test_automatic_suffix_keys_1(self): r''' "RAEUS": "race", "RAEUZ": "raise", "-S": "{^s}", "-Z": "{^s}", RAEUSZ " races" ''' def test_bug719(self): r''' # Glue (&) does not work with "Spaces After". "P*": "{&P}" :spaces_after P*/P*/P*/P*/P*/P* 'PPPPPP ' ''' @pytest.mark.xfail def test_bug741(self): r''' # Uppercase last word also uppercases next word's prefix. "KPA*TS": "{*<}", "TPAO": "foo", "KAUPB": "{con^}", TPAO/KPA*TS/KAUPB/TPAO " FOO confoo" ''' @pytest.mark.xfail def test_carry_capitalization_spacing1(self): r''' 'S-P': '{^ ^}', 'R-R': '{^~|\n^}', S-P/R-R ' \n' ''' @pytest.mark.xfail def test_carry_capitalization_spacing2(self): r'''
class StenoEngine(object): HOOKS = """ stroked translated machine_state_changed output_changed config_changed 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._suggestions = Suggestions(self._dictionaries) self._keyboard_emulation = keyboard_emulation self._hooks = {hook: [] for hook in self.HOOKS} 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): 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 _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"] ) if reset_machine or machine_params != self._machine_params: 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).resolve() 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 dictionaries. dictionaries_files = config["dictionary_file_names"] copy_default_dictionaries(dictionaries_files) dictionaries = self._dictionaries_manager.load(dictionaries_files) self._dictionaries.set_dicts(dictionaries) # Trigger `config_changed` hook. if config_update: self._trigger_hook("config_changed", config_update) def _quit(self): self._stop() 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._trigger_hook("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(":", 2) command_fn = registry.get_plugin("command", command_args[0]).resolve() 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): self._same_thread_hook(self._quit) @with_lock def list_plugins(self, plugin_type): return sorted(registry.list_plugins(plugin_type)) @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 self._suggestions.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=None): if dictionary is None: dictionary = self._dictionaries.dicts[0].get_path() self._dictionaries.set(strokes, translation, dictionary=dictionary) self._dictionaries.save(path_list=(dictionary,)) @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)