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.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')
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 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()])
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')
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 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 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 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)
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')
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 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 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 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'''