From 644f4c5978c316d8a1dd3901478b3f518d34d97f Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sat, 28 Sep 2024 12:42:18 +0200 Subject: [PATCH 01/32] wip --- bin/input-remapper-control | 461 +++++----- bin/input-remapper-gtk | 35 +- bin/input-remapper-reader-service | 17 +- bin/input-remapper-service | 22 +- inputremapper/configs/base_config.py | 2 +- inputremapper/configs/data.py | 2 +- inputremapper/configs/global_config.py | 11 +- inputremapper/configs/input_config.py | 2 +- inputremapper/configs/mapping.py | 7 +- inputremapper/configs/migrations.py | 803 +++++++++--------- inputremapper/configs/paths.py | 246 +++--- inputremapper/configs/preset.py | 6 +- inputremapper/configs/system_mapping.py | 8 +- inputremapper/configs/validation_errors.py | 4 +- inputremapper/daemon.py | 19 +- inputremapper/evdev/__init__.py | 0 inputremapper/evdev/evdev.py | 25 + inputremapper/groups.py | 12 +- inputremapper/gui/autocompletion.py | 2 +- inputremapper/gui/components/device_groups.py | 2 +- inputremapper/gui/components/presets.py | 2 +- inputremapper/gui/controller.py | 6 +- inputremapper/gui/data_manager.py | 42 +- inputremapper/gui/messages/message_broker.py | 2 +- inputremapper/gui/reader_client.py | 2 +- inputremapper/gui/reader_service.py | 8 +- inputremapper/gui/user_interface.py | 2 +- inputremapper/gui/utils.py | 2 +- inputremapper/injection/context.py | 2 +- inputremapper/injection/event_reader.py | 2 +- inputremapper/injection/global_uinputs.py | 45 +- inputremapper/injection/injector.py | 2 +- inputremapper/injection/macros/macro.py | 8 +- inputremapper/injection/macros/parse.py | 2 +- .../mapping_handlers/abs_to_abs_handler.py | 2 +- .../mapping_handlers/abs_to_rel_handler.py | 2 +- .../mapping_handlers/axis_switch_handler.py | 2 +- .../mapping_handlers/combination_handler.py | 2 +- .../injection/mapping_handlers/key_handler.py | 2 +- .../mapping_handlers/macro_handler.py | 2 +- .../mapping_handlers/mapping_handler.py | 2 +- .../mapping_handlers/mapping_parser.py | 2 +- .../mapping_handlers/rel_to_abs_handler.py | 2 +- .../mapping_handlers/rel_to_btn_handler.py | 2 +- .../mapping_handlers/rel_to_rel_handler.py | 2 +- inputremapper/injection/numlock.py | 2 +- inputremapper/input_event.py | 38 +- inputremapper/ipc/pipe.py | 10 +- inputremapper/ipc/shared_dict.py | 2 +- inputremapper/ipc/socket.py | 8 +- inputremapper/logger.py | 336 -------- inputremapper/logger/__init__.py | 0 inputremapper/logger/formatter.py | 142 ++++ inputremapper/logger/logger.py | 183 ++++ inputremapper/user.py | 62 +- tests/__init__.py | 2 - tests/integration/test_components.py | 25 + tests/integration/test_daemon.py | 7 +- tests/integration/test_data.py | 2 + tests/integration/test_gui.py | 32 +- tests/integration/test_numlock.py | 2 + tests/integration/test_user_interface.py | 2 + tests/{test.py => legacy_test_setup.py} | 61 +- tests/lib/cleanup.py | 6 +- tests/lib/fixtures.py | 10 +- tests/lib/logger.py | 4 +- tests/lib/patches.py | 64 +- tests/lib/pipes.py | 8 + tests/new_test.py | 130 +++ tests/unit/__init__.py | 2 - tests/unit/test_config.py | 12 +- tests/unit/test_context.py | 4 +- tests/unit/test_control.py | 145 +++- tests/unit/test_controller.py | 210 +++-- tests/unit/test_daemon.py | 22 +- tests/unit/test_data_manager.py | 86 +- .../test_axis_transformation.py | 2 + .../test_event_pipeline.py | 8 + .../test_mapping_handlers.py | 13 +- tests/unit/test_event_reader.py | 2 + tests/unit/test_global_uinputs.py | 3 + tests/unit/test_groups.py | 11 +- tests/unit/test_injector.py | 3 + tests/unit/test_input_config.py | 3 + tests/unit/test_input_event.py | 2 + tests/unit/test_ipc.py | 4 + tests/unit/test_logger.py | 41 +- tests/unit/test_macros.py | 5 + tests/unit/test_mapping.py | 3 + tests/unit/test_message_broker.py | 3 + tests/unit/test_migrations.py | 232 ++--- tests/unit/test_paths.py | 36 +- tests/unit/test_preset.py | 44 +- tests/unit/test_reader.py | 3 + tests/unit/test_system_mapping.py | 10 +- tests/unit/test_test.py | 2 + tests/unit/test_user.py | 14 +- tests/unit/test_util.py | 2 + 98 files changed, 2282 insertions(+), 1618 deletions(-) create mode 100644 inputremapper/evdev/__init__.py create mode 100644 inputremapper/evdev/evdev.py delete mode 100644 inputremapper/logger.py create mode 100644 inputremapper/logger/__init__.py create mode 100644 inputremapper/logger/formatter.py create mode 100644 inputremapper/logger/logger.py rename tests/{test.py => legacy_test_setup.py} (76%) create mode 100644 tests/new_test.py diff --git a/bin/input-remapper-control b/bin/input-remapper-control index 639ffdb3f..d77b448cf 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -21,93 +21,111 @@ """Control the dbus service from the command line.""" -import os -import sys import argparse import logging +import os import subprocess +import sys +from enum import Enum +from typing import Union, Optional -from inputremapper.logger import logger, update_verbosity, log_info -from inputremapper.configs.migrations import migrate from inputremapper.configs.global_config import global_config +from inputremapper.configs.migrations import Migrations +from inputremapper.logger.logger import logger + +# TODO this sounds like something DI would fix, making everything independent of +# input order: # import inputremapper modules as late as possible to make sure the correct # log level is applied before anything is logged -AUTOLOAD = 'autoload' -START = 'start' -STOP = 'stop' -STOP_ALL = 'stop-all' -HELLO = 'hello' - -# internal stuff that the gui uses -START_DAEMON = 'start-daemon' -START_READER_SERVICE = 'start-reader-service' +class Commands(Enum): + AUTOLOAD = "autoload" + START = "start" + STOP = "stop" + STOP_ALL = "stop-all" + HELLO = "hello" -def run(cmd): - """Run and log a command.""" - logger.info('Running `%s`...', cmd) - code = os.system(cmd) - if code != 0: - logger.error('Failed. exit code %d', code) +class Internals(Enum): + # internal stuff that the gui uses + START_DAEMON = "start-daemon" + START_READER_SERVICE = "start-reader-service" -COMMANDS = [AUTOLOAD, START, STOP, HELLO, STOP_ALL] +class Options: + command: Union[Commands, Internals] + config_dir: str + preset: str + device: str + list_devices: bool + key_names: str + debug: bool + version: str -INTERNALS = [START_DAEMON, START_READER_SERVICE] +class InputRemapperControl: + def run(self, cmd) -> None: + """Run and log a command.""" + logger.info("Running `%s`...", cmd) + code = os.system(cmd) + if code != 0: + logger.error("Failed. exit code %d", code) -def utils(options): - """Listing names, tasks that don't require a running daemon.""" - if options.list_devices: + def list_devices(self): logger.setLevel(logging.ERROR) from inputremapper.groups import groups + for group in groups: print(group.key) - if options.key_names: + def list_key_names(self): from inputremapper.configs.system_mapping import system_mapping - print('\n'.join(system_mapping.list_names())) + print("\n".join(system_mapping.list_names())) -def communicate(options, daemon): - """Commands that require a running daemon.""" - # import stuff late to make sure the correct log level is applied - # before anything is logged - from inputremapper.groups import groups - from inputremapper.configs.paths import USER + def communicate( + self, + command: Commands, + device: str, + config_dir: Optional[str], + preset: str, + ) -> None: + """Commands that require a running daemon.""" + if self.daemon is None: + # probably broken tests + logger.error("Daemon missing") + sys.exit(5) - def require_group(): - if options.device is None: - logger.error('--device missing') - sys.exit(3) + if config_dir is not None: + self._load_config(config_dir) - if options.device.startswith('/dev'): - group = groups.find(path=options.device) - else: - group = groups.find(key=options.device) + self.ensure_migrated() - if group is None: - logger.error( - 'Device "%s" is unknown or not an appropriate input device', - options.device - ) - sys.exit(4) + if command == Commands.AUTOLOAD.value: + self._autoload(device) - return group + if command == Commands.START.value: + self._start(device, preset) + + if command == Commands.STOP.value: + self._stop(device) - if daemon is None: - # probably broken tests - logger.error('Daemon missing') - sys.exit(5) + if command == Commands.STOP_ALL.value: + self.daemon.stop_all() - if options.config_dir is not None: - path = os.path.abspath(os.path.expanduser(os.path.join( - options.config_dir, - 'config.json' - ))) + if command == Commands.HELLO.value: + self._hello() + + def _hello(self): + response = self.daemon.hello("hello") + logger.info('Daemon answered with "%s"', response) + + def _load_config(self, config_dir: str) -> None: + path = os.path.abspath( + os.path.expanduser(os.path.join(config_dir, "config.json")) + ) if not os.path.exists(path): logger.error('"%s" does not exist', path) sys.exit(6) @@ -115,127 +133,153 @@ def communicate(options, daemon): logger.info('Using config from "%s" instead', path) global_config.load_config(path) - if USER != 'root': - # Might be triggered by udev, so skip the root user. - # This will also refresh the config of the daemon if the user changed - # it in the meantime. - # config_dir is either the cli arg or the default path in home - config_dir = os.path.dirname(global_config.path) - daemon.set_config_dir(config_dir) - migrate() + def ensure_migrated(self) -> None: + # import stuff late to make sure the correct log level is applied + # before anything is logged TODO fix this + from inputremapper.user import UserUtils - if options.command == AUTOLOAD: - # if device was specified, autoload for that one. if None autoload - # for all devices. - if options.device is None: - logger.info('Autoloading all') - # timeout is not documented, for more info see - # https://github.com/LEW21/pydbus/blob/master/pydbus/proxy_method.py - daemon.autoload(timeout=10) - else: - group = require_group() - logger.info('Asking daemon to autoload for %s', options.device) - daemon.autoload_single(group.key, timeout=2) + if UserUtils.user != "root": + # Might be triggered by udev, so skip the root user. + # This will also refresh the config of the daemon if the user changed + # it in the meantime. + # config_dir is either the cli arg or the default path in home + config_dir = os.path.dirname(global_config.path) + self.daemon.set_config_dir(config_dir) + Migrations.migrate() + + def _stop(self, device: str) -> None: + group = self._require_group(device) + self.daemon.stop_injecting(group.key) - if options.command == START: - group = require_group() + def _start(self, device: str, preset: str) -> None: + group = self._require_group(device) logger.info( 'Starting injection: "%s", "%s"', - options.device, options.preset + device, + preset, ) - daemon.start_injecting(group.key, options.preset) - - if options.command == STOP: - group = require_group() - daemon.stop_injecting(group.key) - - if options.command == STOP_ALL: - daemon.stop_all() - - if options.command == HELLO: - response = daemon.hello('hello') - logger.info('Daemon answered with "%s"', response) - - -def internals(options): - """Methods that are needed to get the gui to work and that require root. + self.daemon.start_injecting(group.key, preset) - input-remapper-control should be started with sudo or pkexec for this. - """ - debug = ' -d' if options.debug else '' - - if options.command == START_READER_SERVICE: - cmd = f'input-remapper-reader-service{debug}' - elif options.command == START_DAEMON: - cmd = f'input-remapper-service --hide-info{debug}' - else: - return - - # daemonize - cmd = f'{cmd} &' - logger.debug(f'Running `{cmd}`') - os.system(cmd) - - -def _num_logged_in_users(): - """Check how many users are logged in.""" - who = subprocess.run(['who'], stdout=subprocess.PIPE).stdout.decode() - return len([user for user in who.split('\n') if user.strip() != ""]) - - -def _systemd_finished(): - """Check if systemd finished booting.""" - try: - systemd_analyze = subprocess.run(['systemd-analyze'], stdout=subprocess.PIPE) - except FileNotFoundError: - # probably not systemd, lets assume true to not block input-remapper for good - # on certain installations - return True + def _require_group(self, device: str): + # import stuff late to make sure the correct log level is applied + # before anything is logged TODO fix this + from inputremapper.groups import groups - if 'finished' in systemd_analyze.stdout.decode(): - # it writes into stderr otherwise or something - return True + if device is None: + logger.error("--device missing") + sys.exit(3) - return False + if device.startswith("/dev"): + group = groups.find(path=device) + else: + group = groups.find(key=device) + if group is None: + logger.error( + 'Device "%s" is unknown or not an appropriate input device', + device, + ) + sys.exit(4) -def boot_finished(): - """Check if booting is completed.""" - # Get as much information as needed to really safely determine if booting up is - # complete. - # - `who` returns an empty list on some system for security purposes - # - something might be broken and might make systemd_analyze fail: - # Bootup is not yet finished - # (org.freedesktop.systemd1.Manager.FinishTimestampMonotonic=0). - # Please try again later. - # Hint: Use 'systemctl list-jobs' to see active jobs - if _systemd_finished(): - logger.debug('System is booted') - return True + return group - if _num_logged_in_users() > 0: - logger.debug('User(s) logged in') - return True + def _autoload(self, device: str) -> None: + # if device was specified, autoload for that one. if None autoload + # for all devices. + if device is None: + logger.info("Autoloading all") + # timeout is not documented, for more info see + # https://github.com/LEW21/pydbus/blob/master/pydbus/proxy_method.py + self.daemon.autoload(timeout=10) + else: + group = self._require_group(device) + logger.info("Asking daemon to autoload for %s", device) + self.daemon.autoload_single(group.key, timeout=2) - return False + def internals(self, command: Internals, debug: True) -> None: + """Methods that are needed to get the gui to work and that require root. + input-remapper-control should be started with sudo or pkexec for this. + """ + debug = " -d" if debug else "" -def main(options): + if command == Internals.START_READER_SERVICE.value: + cmd = f"input-remapper-reader-service{debug}" + elif command == Internals.START_DAEMON.value: + cmd = f"input-remapper-service --hide-info{debug}" + else: + return + + # daemonize + cmd = f"{cmd} &" + logger.debug(f"Running `{cmd}`") + os.system(cmd) + + def _num_logged_in_users(self) -> int: + """Check how many users are logged in.""" + who = subprocess.run(["who"], stdout=subprocess.PIPE).stdout.decode() + return len([user for user in who.split("\n") if user.strip() != ""]) + + def _is_systemd_finished(self) -> bool: + """Check if systemd finished booting.""" + try: + systemd_analyze = subprocess.run( + ["systemd-analyze"], stdout=subprocess.PIPE + ) + except FileNotFoundError: + # probably not systemd, lets assume true to not block input-remapper for good + # on certain installations + return True + + if "finished" in systemd_analyze.stdout.decode(): + # it writes into stderr otherwise or something + return True + + return False + + def boot_finished(self) -> bool: + """Check if booting is completed.""" + # Get as much information as needed to really safely determine if booting up is + # complete. + # - `who` returns an empty list on some system for security purposes + # - something might be broken and might make systemd_analyze fail: + # Bootup is not yet finished + # (org.freedesktop.systemd1.Manager.FinishTimestampMonotonic=0). + # Please try again later. + # Hint: Use 'systemctl list-jobs' to see active jobs + if self._is_systemd_finished(): + logger.debug("System is booted") + return True + + if self._num_logged_in_users() > 0: + logger.debug("User(s) logged in") + return True + + return False + + def set_daemon(self, daemon): + # TODO DI? + self.daemon = daemon + + +def main(options: Options) -> None: + input_remapper_control = InputRemapperControl() if options.debug: - update_verbosity(True) + logger.update_verbosity(True) if options.version: - log_info() + logger.log_info() return logger.debug('Call for "%s"', sys.argv) - from inputremapper.configs.paths import USER - boot_finished_ = boot_finished() - is_root = USER == "root" - is_autoload = options.command == AUTOLOAD + from inputremapper.user import UserUtils + + boot_finished_ = input_remapper_control.boot_finished() + is_root = UserUtils.user == "root" + is_autoload = options.command == Commands.AUTOLOAD config_dir_set = options.config_dir is not None if is_autoload and not boot_finished_ and is_root and not config_dir_set: # this is probably happening during boot time and got @@ -244,70 +288,109 @@ def main(options): # of confusing service logs. And also avoids potential for problems when # input-remapper-control stresses about evdev, dbus and multiprocessing already # while the system hasn't even booted completely. - logger.warning('Skipping autoload command without a logged in user') + logger.warning("Skipping autoload command without a logged in user") return if options.command is not None: - if options.command in INTERNALS: - internals(options) - elif options.command in COMMANDS: + if options.command in Internals: + input_remapper_control.internals(options.command, options.debug) + elif options.command in Commands: from inputremapper.daemon import Daemon + daemon = Daemon.connect(fallback=False) - communicate(options, daemon) + + input_remapper_control.set_daemon(daemon) + + input_remapper_control.communicate( + options.command, + options.device, + options.config_dir, + options.preset, + ) else: logger.error('Unknown command "%s"', options.command) else: - utils(options) + if options.list_devices: + input_remapper_control.list_devices() + + if options.key_names: + input_remapper_control.key_names() if options.command: - logger.info('Done') + logger.info("Done") -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( - '--command', action='store', dest='command', help=( - 'Communicate with the daemon. Available commands are start, ' - 'stop, autoload, hello or stop-all' - ), default=None, metavar='NAME' + "--command", + action="store", + dest="command", + help=( + "Communicate with the daemon. Available commands are start, " + "stop, autoload, hello or stop-all" + ), + default=None, + metavar="NAME", ) parser.add_argument( - '--config-dir', action='store', dest='config_dir', + "--config-dir", + action="store", + dest="config_dir", help=( - 'path to the config directory containing config.json, ' - 'xmodmap.json and the presets folder. ' - 'defaults to ~/.config/input-remapper/' + "path to the config directory containing config.json, " + "xmodmap.json and the presets folder. " + "defaults to ~/.config/input-remapper/" ), - default=None, metavar='PATH', + default=None, + metavar="PATH", ) parser.add_argument( - '--preset', action='store', dest='preset', - help='The filename of the preset without the .json extension.', - default=None, metavar='NAME', + "--preset", + action="store", + dest="preset", + help="The filename of the preset without the .json extension.", + default=None, + metavar="NAME", ) parser.add_argument( - '--device', action='store', dest='device', - help='One of the device keys from --list-devices', - default=None, metavar='NAME' + "--device", + action="store", + dest="device", + help="One of the device keys from --list-devices", + default=None, + metavar="NAME", ) parser.add_argument( - '--list-devices', action='store_true', dest='list_devices', - help='List available device keys and exit', - default=False + "--list-devices", + action="store_true", + dest="list_devices", + help="List available device keys and exit", + default=False, ) parser.add_argument( - '--symbol-names', action='store_true', dest='key_names', - help='Print all available names for the preset', - default=False + "--symbol-names", + action="store_true", + dest="key_names", + help="Print all available names for the preset", + default=False, ) parser.add_argument( - '-d', '--debug', action='store_true', dest='debug', - help='Displays additional debug information', - default=False + "-d", + "--debug", + action="store_true", + dest="debug", + help="Displays additional debug information", + default=False, ) parser.add_argument( - '-v', '--version', action='store_true', dest='version', - help='Print the version and exit', default=False + "-v", + "--version", + action="store_true", + dest="version", + help="Print the version and exit", + default=False, ) - main(parser.parse_args(sys.argv[1:])) + options = parser.parse_args(sys.argv[1:]) + main(options) diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index 06cdf4977..5ea4493d1 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -22,14 +22,15 @@ from __future__ import annotations -import sys import atexit +import sys from argparse import ArgumentParser import gi -gi.require_version('Gtk', '3.0') -gi.require_version('GLib', '2.0') -gi.require_version('GtkSource', '4') + +gi.require_version("Gtk", "3.0") +gi.require_version("GLib", "2.0") +gi.require_version("GtkSource", "4") from gi.repository import Gtk # https://github.com/Nuitka/Nuitka/issues/607#issuecomment-650217096 @@ -38,7 +39,7 @@ Gtk.init() from inputremapper.gui.gettext import _, LOCALE_DIR from inputremapper.gui.reader_service import ReaderService from inputremapper.daemon import DaemonProxy -from inputremapper.logger import logger, update_verbosity, log_info +from inputremapper.logger.logger import logger def start_processes() -> DaemonProxy: @@ -53,18 +54,21 @@ def start_processes() -> DaemonProxy: return Daemon.connect() -if __name__ == '__main__': +if __name__ == "__main__": parser = ArgumentParser() parser.add_argument( - '-d', '--debug', action='store_true', dest='debug', - help=_('Displays additional debug information'), - default=False + "-d", + "--debug", + action="store_true", + dest="debug", + help=_("Displays additional debug information"), + default=False, ) options = parser.parse_args(sys.argv[1:]) - update_verbosity(options.debug) - log_info('input-remapper-gtk') - logger.debug('Using locale directory: {}'.format(LOCALE_DIR)) + logger.update_verbosity(options.debug) + logger.log_info("input-remapper-gtk") + logger.debug("Using locale directory: {}".format(LOCALE_DIR)) # import input-remapper stuff after setting the log verbosity from inputremapper.gui.messages.message_broker import MessageBroker, MessageType @@ -89,7 +93,12 @@ if __name__ == '__main__': daemon = start_processes() data_manager = DataManager( - message_broker, GlobalConfig(), reader_client, daemon, GlobalUInputs(), system_mapping + message_broker, + GlobalConfig(), + reader_client, + daemon, + GlobalUInputs(), + system_mapping, ) controller = Controller(message_broker, data_manager) user_interface = UserInterface(message_broker, controller) diff --git a/bin/input-remapper-reader-service b/bin/input-remapper-reader-service index c8ca6ba6c..e82b4f1e5 100755 --- a/bin/input-remapper-reader-service +++ b/bin/input-remapper-reader-service @@ -21,21 +21,24 @@ """Starts the root reader-service.""" import asyncio -import os -import sys import atexit +import os import signal +import sys from argparse import ArgumentParser -from inputremapper.logger import update_verbosity from inputremapper.groups import _Groups +from inputremapper.logger.logger import update_verbosity - -if __name__ == '__main__': +if __name__ == "__main__": parser = ArgumentParser() parser.add_argument( - '-d', '--debug', action='store_true', dest='debug', - help='Displays additional debug information', default=False + "-d", + "--debug", + action="store_true", + dest="debug", + help="Displays additional debug information", + default=False, ) options = parser.parse_args(sys.argv[1:]) diff --git a/bin/input-remapper-service b/bin/input-remapper-service index 6d588e4ae..c5fdd269e 100755 --- a/bin/input-remapper-service +++ b/bin/input-remapper-service @@ -25,18 +25,24 @@ import sys from argparse import ArgumentParser -from inputremapper.logger import update_verbosity, log_info +from inputremapper.logger.logger import update_verbosity, log_info - -if __name__ == '__main__': +if __name__ == "__main__": parser = ArgumentParser() parser.add_argument( - '-d', '--debug', action='store_true', dest='debug', - help='Displays additional debug information', default=False + "-d", + "--debug", + action="store_true", + dest="debug", + help="Displays additional debug information", + default=False, ) parser.add_argument( - '--hide-info', action='store_true', dest='hide_info', - help='Don\'t display version information', default=False + "--hide-info", + action="store_true", + dest="hide_info", + help="Don't display version information", + default=False, ) options = parser.parse_args(sys.argv[1:]) @@ -47,7 +53,7 @@ if __name__ == '__main__': from inputremapper.daemon import Daemon if not options.hide_info: - log_info('input-remapper-service') + log_info("input-remapper-service") daemon = Daemon() daemon.publish() diff --git a/inputremapper/configs/base_config.py b/inputremapper/configs/base_config.py index 85a5de588..0bfb8263f 100644 --- a/inputremapper/configs/base_config.py +++ b/inputremapper/configs/base_config.py @@ -22,7 +22,7 @@ import copy from typing import Union, List, Optional, Callable, Any -from inputremapper.logger import logger, VERSION +from inputremapper.logger.logger import logger, VERSION NONE = "none" diff --git a/inputremapper/configs/data.py b/inputremapper/configs/data.py index 7635d2510..65f2d0725 100644 --- a/inputremapper/configs/data.py +++ b/inputremapper/configs/data.py @@ -27,7 +27,7 @@ import pkg_resources -from inputremapper.logger import logger +from inputremapper.logger.logger import logger logged = False diff --git a/inputremapper/configs/global_config.py b/inputremapper/configs/global_config.py index 9513151c2..852a98adb 100644 --- a/inputremapper/configs/global_config.py +++ b/inputremapper/configs/global_config.py @@ -24,8 +24,9 @@ from typing import Optional from inputremapper.configs.base_config import ConfigBase, INITIAL_CONFIG -from inputremapper.configs.paths import CONFIG_PATH, USER, touch -from inputremapper.logger import logger +from inputremapper.configs.paths import PathUtils +from inputremapper.user import UserUtils +from inputremapper.logger.logger import logger MOUSE = "mouse" WHEEL = "wheel" @@ -42,7 +43,7 @@ class GlobalConfig(ConfigBase): """ def __init__(self): - self.path = os.path.join(CONFIG_PATH, "config.json") + self.path = os.path.join(PathUtils.config_path(), "config.json") super().__init__() def get_dir(self) -> str: @@ -118,11 +119,11 @@ def load_config(self, path: Optional[str] = None): def _save_config(self): """Save the config to the file system.""" - if USER == "root": + if UserUtils.user == "root": logger.debug("Skipping config file creation for the root user") return - touch(self.path) + PathUtils.touch(self.path) with open(self.path, "w") as file: json.dump(self._config, file, indent=4) diff --git a/inputremapper/configs/input_config.py b/inputremapper/configs/input_config.py index 1424962b0..b60554f70 100644 --- a/inputremapper/configs/input_config.py +++ b/inputremapper/configs/input_config.py @@ -33,7 +33,7 @@ from inputremapper.configs.system_mapping import system_mapping from inputremapper.gui.messages.message_types import MessageType -from inputremapper.logger import logger +from inputremapper.logger.logger import logger from inputremapper.utils import get_evdev_constant_name # having shift in combinations modifies the configured output, diff --git a/inputremapper/configs/mapping.py b/inputremapper/configs/mapping.py index f98c320d3..f019fcf37 100644 --- a/inputremapper/configs/mapping.py +++ b/inputremapper/configs/mapping.py @@ -73,11 +73,10 @@ SymbolAndCodeMismatchError, MissingMacroOrKeyError, MissingOutputAxisError, - MacroParsingError, ) from inputremapper.gui.gettext import _ from inputremapper.gui.messages.message_types import MessageType -from inputremapper.injection.global_uinputs import can_default_uinput_emit +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.macros.parse import is_this_a_macro, parse from inputremapper.utils import get_evdev_constant_name @@ -393,7 +392,9 @@ def validate_symbol(cls, values): raise OutputSymbolUnknownError(symbol) target = values.get("target_uinput") - if target is not None and not can_default_uinput_emit(target, EV_KEY, code): + if target is not None and not GlobalUInputs.can_default_uinput_emit( + target, EV_KEY, code + ): raise SymbolNotAvailableInTargetError(symbol, target) return values diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index 9b65cc73d..f5ca9dc56 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -30,7 +30,7 @@ import re import shutil from pathlib import Path -from typing import Iterator, Tuple, Dict, List +from typing import Iterator, Tuple, Dict, List, Optional, TypedDict import pkg_resources from evdev.ecodes import ( @@ -49,459 +49,476 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping, UIMapping -from inputremapper.configs.paths import get_preset_path, mkdir, CONFIG_PATH, remove +from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.configs.system_mapping import system_mapping from inputremapper.injection.global_uinputs import global_uinputs from inputremapper.injection.macros.parse import is_this_a_macro -from inputremapper.logger import logger, VERSION -from inputremapper.user import HOME +from inputremapper.logger.logger import logger, VERSION +from inputremapper.user import UserUtils -def all_presets() -> Iterator[Tuple[os.PathLike, Dict | List]]: - """Get all presets for all groups as list.""" - if not os.path.exists(get_preset_path()): - return +class Config(TypedDict): + input_combination: Optional[InputCombination] + target_uinput: str + output_type: int + output_code: Optional[int] - preset_path = Path(get_preset_path()) - for folder in preset_path.iterdir(): - if not folder.is_dir(): - continue - for preset in folder.iterdir(): - if preset.suffix != ".json": - continue +class Migrations: + @staticmethod + def migrate(): + """Migrate config files to the current release.""" - try: - with open(preset, "r") as f: - preset_structure = json.load(f) - yield preset, preset_structure - except json.decoder.JSONDecodeError: - logger.warning('Invalid json format in preset "%s"', preset) - continue + Migrations._rename_to_input_remapper() + Migrations._copy_to_v2() -def config_version(): - """Get the version string in config.json as packaging.Version object.""" - config_path = os.path.join(CONFIG_PATH, "config.json") + v = Migrations.config_version() - if not os.path.exists(config_path): - return pkg_resources.parse_version("0.0.0") + if v < pkg_resources.parse_version("0.4.0"): + Migrations._config_suffix() + Migrations._preset_path() - with open(config_path, "r") as file: - config = json.load(file) + if v < pkg_resources.parse_version("1.2.2"): + Migrations._mapping_keys() - if "version" in config.keys(): - return pkg_resources.parse_version(config["version"]) + if v < pkg_resources.parse_version("1.4.0"): + global_uinputs.prepare_all() + Migrations._add_target() - return pkg_resources.parse_version("0.0.0") + if v < pkg_resources.parse_version("1.4.1"): + Migrations._otherwise_to_else() + if v < pkg_resources.parse_version("1.5.0"): + Migrations._remove_logs() -def _config_suffix(): - """Append the .json suffix to the config file.""" - deprecated_path = os.path.join(CONFIG_PATH, "config") - config_path = os.path.join(CONFIG_PATH, "config.json") - if os.path.exists(deprecated_path) and not os.path.exists(config_path): - logger.info('Moving "%s" to "%s"', deprecated_path, config_path) - os.rename(deprecated_path, config_path) + if v < pkg_resources.parse_version("1.6.0-beta"): + Migrations._convert_to_individual_mappings() + # add new migrations here -def _preset_path(): - """Migrate the folder structure from < 0.4.0. + if v < pkg_resources.parse_version(VERSION): + Migrations._update_version() - Move existing presets into the new subfolder 'presets' - """ - new_preset_folder = os.path.join(CONFIG_PATH, "presets") - if os.path.exists(get_preset_path()) or not os.path.exists(CONFIG_PATH): - return + @staticmethod + def all_presets() -> Iterator[Tuple[os.PathLike, Dict | List]]: + """Get all presets for all groups as list.""" + if not os.path.exists(PathUtils.get_preset_path()): + return - logger.info("Migrating presets from < 0.4.0...") - groups = os.listdir(CONFIG_PATH) - mkdir(get_preset_path()) - for group in groups: - path = os.path.join(CONFIG_PATH, group) - if os.path.isdir(path): - target = path.replace(CONFIG_PATH, new_preset_folder) - logger.info('Moving "%s" to "%s"', path, target) - os.rename(path, target) - - logger.info("done") + preset_path = Path(PathUtils.get_preset_path()) + for folder in preset_path.iterdir(): + if not folder.is_dir(): + continue + for preset in folder.iterdir(): + if preset.suffix != ".json": + continue -def _mapping_keys(): - """Update all preset mappings. + try: + with open(preset, "r") as f: + preset_structure = json.load(f) + yield preset, preset_structure + except json.decoder.JSONDecodeError: + logger.warning('Invalid json format in preset "%s"', preset) + continue - Update all keys in preset to include value e.g.: '1,5'->'1,5,1' - """ - for preset, preset_structure in all_presets(): - if isinstance(preset_structure, list): - continue # the preset must be at least 1.6-beta version + @staticmethod + def config_version(): + """Get the version string in config.json as packaging.Version object.""" + config_path = os.path.join(PathUtils.config_path(), "config.json") - changes = 0 - if "mapping" in preset_structure.keys(): - mapping = copy.deepcopy(preset_structure["mapping"]) - for key in mapping.keys(): - if key.count(",") == 1: - preset_structure["mapping"][f"{key},1"] = preset_structure[ - "mapping" - ].pop(key) - changes += 1 + if not os.path.exists(config_path): + return pkg_resources.parse_version("0.0.0") - if changes: - with open(preset, "w") as file: - logger.info('Updating mapping keys of "%s"', preset) - json.dump(preset_structure, file, indent=4) - file.write("\n") + with open(config_path, "r") as file: + config = json.load(file) + if "version" in config.keys(): + return pkg_resources.parse_version(config["version"]) -def _update_version(): - """Write the current version to the config file.""" - config_file = os.path.join(CONFIG_PATH, "config.json") - if not os.path.exists(config_file): - return + return pkg_resources.parse_version("0.0.0") - with open(config_file, "r") as file: - config = json.load(file) + @staticmethod + def _config_suffix(): + """Append the .json suffix to the config file.""" + deprecated_path = os.path.join(PathUtils.config_path(), "config") + config_path = os.path.join(PathUtils.config_path(), "config.json") + if os.path.exists(deprecated_path) and not os.path.exists(config_path): + logger.info('Moving "%s" to "%s"', deprecated_path, config_path) + os.rename(deprecated_path, config_path) + + @staticmethod + def _preset_path(): + """Migrate the folder structure from < 0.4.0. + + Move existing presets into the new subfolder 'presets' + """ + new_preset_folder = os.path.join(PathUtils.config_path(), "presets") + if os.path.exists(PathUtils.get_preset_path()) or not os.path.exists( + PathUtils.config_path() + ): + return + + logger.info("Migrating presets from < 0.4.0...") + groups = os.listdir(PathUtils.config_path()) + PathUtils.mkdir(PathUtils.get_preset_path()) + for group in groups: + path = os.path.join(PathUtils.config_path(), group) + if os.path.isdir(path): + target = path.replace(PathUtils.config_path(), new_preset_folder) + logger.info('Moving "%s" to "%s"', path, target) + os.rename(path, target) + + logger.info("done") + + @staticmethod + def _mapping_keys(): + """Update all preset mappings. + + Update all keys in preset to include value e.g.: '1,5'->'1,5,1' + """ + for preset, preset_structure in Migrations.all_presets(): + if isinstance(preset_structure, list): + continue # the preset must be at least 1.6-beta version + + changes = 0 + if "mapping" in preset_structure.keys(): + mapping = copy.deepcopy(preset_structure["mapping"]) + for key in mapping.keys(): + if key.count(",") == 1: + preset_structure["mapping"][f"{key},1"] = preset_structure[ + "mapping" + ].pop(key) + changes += 1 + + if changes: + with open(preset, "w") as file: + logger.info('Updating mapping keys of "%s"', preset) + json.dump(preset_structure, file, indent=4) + file.write("\n") + + @staticmethod + def _update_version(): + """Write the current version to the config file.""" + config_file = os.path.join(PathUtils.config_path(), "config.json") + if not os.path.exists(config_file): + return + + with open(config_file, "r") as file: + config = json.load(file) + + config["version"] = VERSION + with open(config_file, "w") as file: + logger.info('Updating version in config to "%s"', VERSION) + json.dump(config, file, indent=4) + + @staticmethod + def _rename_to_input_remapper(): + """Rename .config/key-mapper to .config/input-remapper.""" + old_config_path = os.path.join(UserUtils.home, ".config/key-mapper") + if not os.path.exists(PathUtils.config_path()) and os.path.exists( + old_config_path + ): + logger.info("Moving %s to %s", old_config_path, PathUtils.config_path()) + shutil.move(old_config_path, PathUtils.config_path()) - config["version"] = VERSION - with open(config_file, "w") as file: - logger.info('Updating version in config to "%s"', VERSION) - json.dump(config, file, indent=4) + @staticmethod + def _find_target(symbol): + """Try to find a uinput with the required capabilities for the symbol.""" + capabilities = {EV_KEY: set(), EV_REL: set()} + if is_this_a_macro(symbol): + # deprecated mechanic, cannot figure this out anymore + # capabilities = parse(symbol).get_capabilities() + return None -def _rename_to_input_remapper(): - """Rename .config/key-mapper to .config/input-remapper.""" - old_config_path = os.path.join(HOME, ".config/key-mapper") - if not os.path.exists(CONFIG_PATH) and os.path.exists(old_config_path): - logger.info("Moving %s to %s", old_config_path, CONFIG_PATH) - shutil.move(old_config_path, CONFIG_PATH) + capabilities[EV_KEY] = {system_mapping.get(symbol)} + if len(capabilities[EV_REL]) > 0: + return "mouse" -def _find_target(symbol): - """Try to find a uinput with the required capabilities for the symbol.""" - capabilities = {EV_KEY: set(), EV_REL: set()} + for name, uinput in global_uinputs.devices.items(): + if capabilities[EV_KEY].issubset(uinput.capabilities()[EV_KEY]): + return name - if is_this_a_macro(symbol): - # deprecated mechanic, cannot figure this out anymore - # capabilities = parse(symbol).get_capabilities() + logger.info('could not find a suitable target UInput for "%s"', symbol) return None - capabilities[EV_KEY] = {system_mapping.get(symbol)} - - if len(capabilities[EV_REL]) > 0: - return "mouse" - - for name, uinput in global_uinputs.devices.items(): - if capabilities[EV_KEY].issubset(uinput.capabilities()[EV_KEY]): - return name + @staticmethod + def _add_target(): + """Add the target field to each preset mapping.""" + for preset, preset_structure in Migrations.all_presets(): + if isinstance(preset_structure, list): + continue - logger.info('could not find a suitable target UInput for "%s"', symbol) - return None + if "mapping" not in preset_structure.keys(): + continue + changed = False + for key, symbol in preset_structure["mapping"].copy().items(): + if isinstance(symbol, list): + continue -def _add_target(): - """Add the target field to each preset mapping.""" - for preset, preset_structure in all_presets(): - if isinstance(preset_structure, list): - continue + target = Migrations._find_target(symbol) + if target is None: + target = "keyboard" + symbol = ( + f"{symbol}\n" + "# Broken mapping:\n" + "# No target can handle all specified keycodes" + ) - if "mapping" not in preset_structure.keys(): - continue + logger.info( + 'Changing target of mapping for "%s" in preset "%s" to "%s"', + key, + preset, + target, + ) + symbol = [symbol, target] + preset_structure["mapping"][key] = symbol + changed = True - changed = False - for key, symbol in preset_structure["mapping"].copy().items(): - if isinstance(symbol, list): + if not changed: continue - target = _find_target(symbol) - if target is None: - target = "keyboard" - symbol = ( - f"{symbol}\n" - "# Broken mapping:\n" - "# No target can handle all specified keycodes" - ) + with open(preset, "w") as file: + logger.info('Adding targets for "%s"', preset) + json.dump(preset_structure, file, indent=4) + file.write("\n") - logger.info( - 'Changing target of mapping for "%s" in preset "%s" to "%s"', - key, - preset, - target, - ) - symbol = [symbol, target] - preset_structure["mapping"][key] = symbol - changed = True + @staticmethod + def _otherwise_to_else(): + """Conditional macros should use an "else" parameter instead of "otherwise".""" + for preset, preset_structure in Migrations.all_presets(): + if isinstance(preset_structure, list): + continue - if not changed: - continue + if "mapping" not in preset_structure.keys(): + continue - with open(preset, "w") as file: - logger.info('Adding targets for "%s"', preset) - json.dump(preset_structure, file, indent=4) - file.write("\n") + changed = False + for key, symbol in preset_structure["mapping"].copy().items(): + if not is_this_a_macro(symbol[0]): + continue + symbol_before = symbol[0] + symbol[0] = re.sub(r"otherwise\s*=\s*", "else=", symbol[0]) -def _otherwise_to_else(): - """Conditional macros should use an "else" parameter instead of "otherwise".""" - for preset, preset_structure in all_presets(): - if isinstance(preset_structure, list): - continue + if symbol_before == symbol[0]: + continue - if "mapping" not in preset_structure.keys(): - continue + changed = changed or symbol_before != symbol[0] - changed = False - for key, symbol in preset_structure["mapping"].copy().items(): - if not is_this_a_macro(symbol[0]): - continue + logger.info( + 'Changing mapping for "%s" in preset "%s" to "%s"', + key, + preset, + symbol[0], + ) - symbol_before = symbol[0] - symbol[0] = re.sub(r"otherwise\s*=\s*", "else=", symbol[0]) + preset_structure["mapping"][key] = symbol - if symbol_before == symbol[0]: + if not changed: continue - changed = changed or symbol_before != symbol[0] + with open(preset, "w") as file: + logger.info('Changing otherwise to else for "%s"', preset) + json.dump(preset_structure, file, indent=4) + file.write("\n") - logger.info( - 'Changing mapping for "%s" in preset "%s" to "%s"', - key, - preset, - symbol[0], + @staticmethod + def _input_combination_from_string(combination_string: str) -> InputCombination: + configs = [] + for event_str in combination_string.split("+"): + type_, code, analog_threshold = event_str.split(",") + configs.append( + { + "type": int(type_), + "code": int(code), + "analog_threshold": int(analog_threshold), + } ) - preset_structure["mapping"][key] = symbol - - if not changed: - continue - - with open(preset, "w") as file: - logger.info('Changing otherwise to else for "%s"', preset) - json.dump(preset_structure, file, indent=4) - file.write("\n") + return InputCombination(configs) + @staticmethod + def _convert_to_individual_mappings(): + """Convert preset.json + from {key: [symbol, target]} + to [{input_combination: ..., output_symbol: symbol, ...}] + """ -def _input_combination_from_string(combination_string: str) -> InputCombination: - configs = [] - for event_str in combination_string.split("+"): - type_, code, analog_threshold = event_str.split(",") - configs.append( - { - "type": int(type_), - "code": int(code), - "analog_threshold": int(analog_threshold), - } - ) - - return InputCombination(configs) - - -def _convert_to_individual_mappings(): - """Convert preset.json - from {key: [symbol, target]} - to [{input_combination: ..., output_symbol: symbol, ...}] - """ - - for old_preset_path, old_preset in all_presets(): - if isinstance(old_preset, list): - continue + for old_preset_path, old_preset in Migrations.all_presets(): + if isinstance(old_preset, list): + continue - migrated_preset = Preset(old_preset_path, UIMapping) - if "mapping" in old_preset.keys(): - for combination, symbol_target in old_preset["mapping"].items(): - logger.info( - 'migrating from "%s: %s" to mapping dict', - combination, - symbol_target, - ) - try: - combination = _input_combination_from_string(combination) - except ValueError: - logger.error( - "unable to migrate mapping with invalid combination %s", + migrated_preset = Preset(old_preset_path, UIMapping) + if "mapping" in old_preset.keys(): + for combination, symbol_target in old_preset["mapping"].items(): + logger.info( + 'migrating from "%s: %s" to mapping dict', combination, + symbol_target, ) - continue - - mapping = UIMapping( - input_combination=combination, - target_uinput=symbol_target[1], - output_symbol=symbol_target[0], - ) - migrated_preset.add(mapping) - - if ( - "gamepad" in old_preset.keys() - and "joystick" in old_preset["gamepad"].keys() - ): - joystick_dict = old_preset["gamepad"]["joystick"] - left_purpose = joystick_dict.get("left_purpose") - right_purpose = joystick_dict.get("right_purpose") - # TODO if pointer_speed is migrated, why is it in my config? - pointer_speed = joystick_dict.get("pointer_speed") - if pointer_speed: - pointer_speed /= 100 - non_linearity = joystick_dict.get("non_linearity") # Todo - x_scroll_speed = joystick_dict.get("x_scroll_speed") - y_scroll_speed = joystick_dict.get("y_scroll_speed") - - cfg = { - "input_combination": None, - "target_uinput": "mouse", - "output_type": EV_REL, - "output_code": None, - } - - if left_purpose == "mouse": - x_config = cfg.copy() - y_config = cfg.copy() - x_config["input_combination"] = InputCombination( - [InputConfig(type=EV_ABS, code=ABS_X)] - ) - y_config["input_combination"] = InputCombination( - [InputConfig(type=EV_ABS, code=ABS_Y)] - ) - x_config["output_code"] = REL_X - y_config["output_code"] = REL_Y - mapping_x = Mapping(**x_config) - mapping_y = Mapping(**y_config) - if pointer_speed: - mapping_x.gain = pointer_speed - mapping_y.gain = pointer_speed - migrated_preset.add(mapping_x) - migrated_preset.add(mapping_y) - - if right_purpose == "mouse": - x_config = cfg.copy() - y_config = cfg.copy() - x_config["input_combination"] = InputCombination( - [InputConfig(type=EV_ABS, code=ABS_RX)] - ) - y_config["input_combination"] = InputCombination( - [InputConfig(type=EV_ABS, code=ABS_RY)] - ) - x_config["output_code"] = REL_X - y_config["output_code"] = REL_Y - mapping_x = Mapping(**x_config) - mapping_y = Mapping(**y_config) + try: + combination = Migrations._input_combination_from_string( + combination + ) + except ValueError: + logger.error( + "unable to migrate mapping with invalid combination %s", + combination, + ) + continue + + mapping = UIMapping( + input_combination=combination, + target_uinput=symbol_target[1], + output_symbol=symbol_target[0], + ) + migrated_preset.add(mapping) + + if ( + "gamepad" in old_preset.keys() + and "joystick" in old_preset["gamepad"].keys() + ): + joystick_dict = old_preset["gamepad"]["joystick"] + left_purpose = joystick_dict.get("left_purpose") + right_purpose = joystick_dict.get("right_purpose") + # TODO if pointer_speed is migrated, why is it in my config? + pointer_speed = joystick_dict.get("pointer_speed") if pointer_speed: - mapping_x.gain = pointer_speed - mapping_y.gain = pointer_speed - migrated_preset.add(mapping_x) - migrated_preset.add(mapping_y) - - if left_purpose == "wheel": - x_config = cfg.copy() - y_config = cfg.copy() - x_config["input_combination"] = InputCombination( - [InputConfig(type=EV_ABS, code=ABS_X)] - ) - y_config["input_combination"] = InputCombination( - [InputConfig(type=EV_ABS, code=ABS_Y)] - ) - x_config["output_code"] = REL_HWHEEL_HI_RES - y_config["output_code"] = REL_WHEEL_HI_RES - mapping_x = Mapping(**x_config) - mapping_y = Mapping(**y_config) - if x_scroll_speed: - mapping_x.gain = x_scroll_speed - if y_scroll_speed: - mapping_y.gain = y_scroll_speed - migrated_preset.add(mapping_x) - migrated_preset.add(mapping_y) - - if right_purpose == "wheel": - x_config = cfg.copy() - y_config = cfg.copy() - x_config["input_combination"] = InputCombination( - [InputConfig(type=EV_ABS, code=ABS_RX)] - ) - y_config["input_combination"] = InputCombination( - [InputConfig(type=EV_ABS, code=ABS_RY)] - ) - x_config["output_code"] = REL_HWHEEL_HI_RES - y_config["output_code"] = REL_WHEEL_HI_RES - mapping_x = Mapping(**x_config) - mapping_y = Mapping(**y_config) - if x_scroll_speed: - mapping_x.gain = x_scroll_speed - if y_scroll_speed: - mapping_y.gain = y_scroll_speed - migrated_preset.add(mapping_x) - migrated_preset.add(mapping_y) - - migrated_preset.save() - - -def _copy_to_v2(): - """Move the beta config to the v2 path, or copy the v1 config to the v2 path.""" - # TODO test - if os.path.exists(CONFIG_PATH): - # don't copy to already existing folder - # users should delete the input-remapper-2 folder if they need to - return - - # prioritize the v1 configs over beta configs - old_path = os.path.join(HOME, ".config/input-remapper") - if os.path.exists(os.path.join(old_path, "config.json")): - # no beta path, only old presets exist. COPY to v2 path, which will then be - # migrated by the various migrations. - logger.debug("copying all from %s to %s", old_path, CONFIG_PATH) - shutil.copytree(old_path, CONFIG_PATH) - return - - # if v1 configs don't exist, try to find beta configs. - beta_path = os.path.join(HOME, ".config/input-remapper/beta_1.6.0-beta") - if os.path.exists(beta_path): - # There has never been a different version than "1.6.0-beta" in beta, so we - # only need to check for that exact directory - # already migrated, possibly new presets in them, move to v2 path - logger.debug("moving %s to %s", beta_path, CONFIG_PATH) - shutil.move(beta_path, CONFIG_PATH) - - -def _remove_logs(): - """We will try to rely on journalctl for this in the future.""" - try: - remove(f"{HOME}/.log/input-remapper") - remove("/var/log/input-remapper") - remove("/var/log/input-remapper-control") - except Exception as error: - logger.debug("Failed to remove deprecated logfiles: %s", str(error)) - # this migration is not important. Continue - pass - - -def migrate(): - """Migrate config files to the current release.""" - - _rename_to_input_remapper() - - _copy_to_v2() - - v = config_version() - - if v < pkg_resources.parse_version("0.4.0"): - _config_suffix() - _preset_path() - - if v < pkg_resources.parse_version("1.2.2"): - _mapping_keys() - - if v < pkg_resources.parse_version("1.4.0"): - global_uinputs.prepare_all() - _add_target() - - if v < pkg_resources.parse_version("1.4.1"): - _otherwise_to_else() - - if v < pkg_resources.parse_version("1.5.0"): - _remove_logs() - - if v < pkg_resources.parse_version("1.6.0-beta"): - _convert_to_individual_mappings() - - # add new migrations here - - if v < pkg_resources.parse_version(VERSION): - _update_version() + pointer_speed /= 100 + non_linearity = joystick_dict.get("non_linearity") # Todo + x_scroll_speed = joystick_dict.get("x_scroll_speed") + y_scroll_speed = joystick_dict.get("y_scroll_speed") + + cfg: Config = { + "input_combination": None, + "target_uinput": "mouse", + "output_type": EV_REL, + "output_code": None, + } + + if left_purpose == "mouse": + x_config = cfg.copy() + y_config = cfg.copy() + x_config["input_combination"] = InputCombination( + [InputConfig(type=EV_ABS, code=ABS_X)] + ) + y_config["input_combination"] = InputCombination( + [InputConfig(type=EV_ABS, code=ABS_Y)] + ) + x_config["output_code"] = REL_X + y_config["output_code"] = REL_Y + mapping_x = Mapping(**x_config) + mapping_y = Mapping(**y_config) + if pointer_speed: + mapping_x.gain = pointer_speed + mapping_y.gain = pointer_speed + migrated_preset.add(mapping_x) + migrated_preset.add(mapping_y) + + if right_purpose == "mouse": + x_config = cfg.copy() + y_config = cfg.copy() + x_config["input_combination"] = InputCombination( + [InputConfig(type=EV_ABS, code=ABS_RX)] + ) + y_config["input_combination"] = InputCombination( + [InputConfig(type=EV_ABS, code=ABS_RY)] + ) + x_config["output_code"] = REL_X + y_config["output_code"] = REL_Y + mapping_x = Mapping(**x_config) + mapping_y = Mapping(**y_config) + if pointer_speed: + mapping_x.gain = pointer_speed + mapping_y.gain = pointer_speed + migrated_preset.add(mapping_x) + migrated_preset.add(mapping_y) + + if left_purpose == "wheel": + x_config = cfg.copy() + y_config = cfg.copy() + x_config["input_combination"] = InputCombination( + [InputConfig(type=EV_ABS, code=ABS_X)] + ) + y_config["input_combination"] = InputCombination( + [InputConfig(type=EV_ABS, code=ABS_Y)] + ) + x_config["output_code"] = REL_HWHEEL_HI_RES + y_config["output_code"] = REL_WHEEL_HI_RES + mapping_x = Mapping(**x_config) + mapping_y = Mapping(**y_config) + if x_scroll_speed: + mapping_x.gain = x_scroll_speed + if y_scroll_speed: + mapping_y.gain = y_scroll_speed + migrated_preset.add(mapping_x) + migrated_preset.add(mapping_y) + + if right_purpose == "wheel": + x_config = cfg.copy() + y_config = cfg.copy() + x_config["input_combination"] = InputCombination( + [InputConfig(type=EV_ABS, code=ABS_RX)] + ) + y_config["input_combination"] = InputCombination( + [InputConfig(type=EV_ABS, code=ABS_RY)] + ) + x_config["output_code"] = REL_HWHEEL_HI_RES + y_config["output_code"] = REL_WHEEL_HI_RES + mapping_x = Mapping(**x_config) + mapping_y = Mapping(**y_config) + if x_scroll_speed: + mapping_x.gain = x_scroll_speed + if y_scroll_speed: + mapping_y.gain = y_scroll_speed + migrated_preset.add(mapping_x) + migrated_preset.add(mapping_y) + + migrated_preset.save() + + @staticmethod + def _copy_to_v2(): + """Move the beta config to the v2 path, or copy the v1 config to the v2 path.""" + # TODO test + if os.path.exists(PathUtils.config_path()): + # don't copy to already existing folder + # users should delete the input-remapper-2 folder if they need to + return + + # prioritize the v1 configs over beta configs + old_path = os.path.join(UserUtils.home, ".config/input-remapper") + if os.path.exists(os.path.join(old_path, "config.json")): + # no beta path, only old presets exist. COPY to v2 path, which will then be + # migrated by the various migrations. + logger.debug("copying all from %s to %s", old_path, PathUtils.config_path()) + shutil.copytree(old_path, PathUtils.config_path()) + return + + # if v1 configs don't exist, try to find beta configs. + beta_path = os.path.join( + UserUtils.home, ".config/input-remapper/beta_1.6.0-beta" + ) + if os.path.exists(beta_path): + # There has never been a different version than "1.6.0-beta" in beta, so we + # only need to check for that exact directory + # already migrated, possibly new presets in them, move to v2 path + logger.debug("moving %s to %s", beta_path, PathUtils.config_path()) + shutil.move(beta_path, PathUtils.config_path()) + + @staticmethod + def _remove_logs(): + """We will try to rely on journalctl for this in the future.""" + try: + PathUtils.remove(f"{UserUtils.home}/.log/input-remapper") + PathUtils.remove("/var/log/input-remapper") + PathUtils.remove("/var/log/input-remapper-control") + except Exception as error: + logger.debug("Failed to remove deprecated logfiles: %s", str(error)) + # this migration is not important. Continue + pass diff --git a/inputremapper/configs/paths.py b/inputremapper/configs/paths.py index 54ac5ccde..6e62a7f53 100644 --- a/inputremapper/configs/paths.py +++ b/inputremapper/configs/paths.py @@ -26,122 +26,130 @@ import shutil from typing import List, Union, Optional -from inputremapper.logger import logger, VERSION -from inputremapper.user import USER, HOME - -rel_path = ".config/input-remapper-2" - -CONFIG_PATH = os.path.join(HOME, rel_path) - - -def chown(path): - """Set the owner of a path to the user.""" - try: - shutil.chown(path, user=USER, group=USER) - except LookupError: - # the users group was unknown in one case for whatever reason - shutil.chown(path, user=USER) - - -def touch(path: Union[str, os.PathLike], log=True): - """Create an empty file and all its parent dirs, give it to the user.""" - if str(path).endswith("/"): - raise ValueError(f"Expected path to not end with a slash: {path}") - - if os.path.exists(path): - return - - if log: - logger.info('Creating file "%s"', path) - - mkdir(os.path.dirname(path), log=False) - - os.mknod(path) - chown(path) - - -def mkdir(path, log=True): - """Create a folder, give it to the user.""" - if path == "" or path is None: - return - - if os.path.exists(path): - return - - if log: - logger.info('Creating dir "%s"', path) - - # give all newly created folders to the user. - # e.g. if .config/input-remapper/mouse/ is created the latter two - base = os.path.split(path)[0] - mkdir(base, log=False) - - os.makedirs(path) - chown(path) - - -def split_all(path: Union[os.PathLike, str]) -> List[str]: - """Split the path into its segments.""" - parts = [] - while True: - path, tail = os.path.split(path) - parts.append(tail) - if path == os.path.sep: - # we arrived at the root '/' - parts.append(path) - break - if not path: - # arrived at start of relative path - break - - parts.reverse() - return parts - - -def remove(path): - """Remove whatever is at the path.""" - if not os.path.exists(path): - return - - if os.path.isdir(path): - shutil.rmtree(path) - else: - os.remove(path) - - -def sanitize_path_component(group_name: str) -> str: - """Replace characters listed in - https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words - with an underscore. - """ - for character in '/\\?%*:|"<>': - if character in group_name: - group_name = group_name.replace(character, "_") - return group_name - - -def get_preset_path(group_name: Optional[str] = None, preset: Optional[str] = None): - """Get a path to the stored preset, or to store a preset to.""" - presets_base = os.path.join(CONFIG_PATH, "presets") - - if group_name is None: - return presets_base - - group_name = sanitize_path_component(group_name) - - if preset is not None: - # the extension of the preset should not be shown in the ui. - # if a .json extension arrives this place, it has not been - # stripped away properly prior to this. - if not preset.endswith(".json"): - preset = f"{preset}.json" - - if preset is None: - return os.path.join(presets_base, group_name) - - return os.path.join(presets_base, group_name, preset) - - -def get_config_path(*paths) -> str: - """Get a path in ~/.config/input-remapper/.""" - return os.path.join(CONFIG_PATH, *paths) +from inputremapper.logger.logger import logger +from inputremapper.user import UserUtils + + +# TODO maybe this could become, idk, ConfigService and PresetService +class PathUtils: + rel_path = ".config/input-remapper-2" + + @staticmethod + def config_path() -> str: + # TODO when proper DI is being done, construct PathUtils and configure it in + # the constructor. Then there is no need to recompute the config_path + # each time. Tests might have overwritten UserUtils.home. + return os.path.join(UserUtils.home, PathUtils.rel_path) + + @staticmethod + def chown(path): + """Set the owner of a path to the user.""" + try: + shutil.chown(path, user=UserUtils.user, group=UserUtils.user) + except LookupError: + # the users group was unknown in one case for whatever reason + shutil.chown(path, user=UserUtils.user) + + @staticmethod + def touch(path: Union[str, os.PathLike], log=True): + """Create an empty file and all its parent dirs, give it to the user.""" + if str(path).endswith("/"): + raise ValueError(f"Expected path to not end with a slash: {path}") + + if os.path.exists(path): + return + + if log: + logger.info('Creating file "%s"', path) + + PathUtils.mkdir(os.path.dirname(path), log=False) + + os.mknod(path) + PathUtils.chown(path) + + @staticmethod + def mkdir(path, log=True): + """Create a folder, give it to the user.""" + if path == "" or path is None: + return + + if os.path.exists(path): + return + + if log: + logger.info('Creating dir "%s"', path) + + # give all newly created folders to the user. + # e.g. if .config/input-remapper/mouse/ is created the latter two + base = os.path.split(path)[0] + PathUtils.mkdir(base, log=False) + + os.makedirs(path) + PathUtils.chown(path) + + @staticmethod + def split_all(path: Union[os.PathLike, str]) -> List[str]: + """Split the path into its segments.""" + parts = [] + while True: + path, tail = os.path.split(path) + parts.append(tail) + if path == os.path.sep: + # we arrived at the root '/' + parts.append(path) + break + if not path: + # arrived at start of relative path + break + + parts.reverse() + return parts + + @staticmethod + def remove(path): + """Remove whatever is at the path.""" + if not os.path.exists(path): + return + + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + + @staticmethod + def sanitize_path_component(group_name: str) -> str: + """Replace characters listed in + https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words + with an underscore. + """ + for character in '/\\?%*:|"<>': + if character in group_name: + group_name = group_name.replace(character, "_") + return group_name + + @staticmethod + def get_preset_path(group_name: Optional[str] = None, preset: Optional[str] = None): + """Get a path to the stored preset, or to store a preset to.""" + presets_base = os.path.join(PathUtils.config_path(), "presets") + + if group_name is None: + return presets_base + + group_name = PathUtils.sanitize_path_component(group_name) + + if preset is not None: + # the extension of the preset should not be shown in the ui. + # if a .json extension arrives this place, it has not been + # stripped away properly prior to this. + if not preset.endswith(".json"): + preset = f"{preset}.json" + + if preset is None: + return os.path.join(presets_base, group_name) + + return os.path.join(presets_base, group_name, preset) + + @staticmethod + def get_config_path(*paths) -> str: + """Get a path in ~/.config/input-remapper/.""" + return os.path.join(PathUtils.config_path(), *paths) diff --git a/inputremapper/configs/preset.py b/inputremapper/configs/preset.py index c1cce2f44..af80c636c 100644 --- a/inputremapper/configs/preset.py +++ b/inputremapper/configs/preset.py @@ -43,8 +43,8 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping, UIMapping -from inputremapper.configs.paths import touch -from inputremapper.logger import logger +from inputremapper.configs.paths import PathUtils +from inputremapper.logger.logger import logger MappingModel = TypeVar("MappingModel", bound=UIMapping) @@ -175,7 +175,7 @@ def save(self) -> None: logger.debug("unable to save preset without a path set Preset.path first") return - touch(self.path) + PathUtils.touch(self.path) if not self.has_unsaved_changes(): logger.debug("Not saving unchanged preset") return diff --git a/inputremapper/configs/system_mapping.py b/inputremapper/configs/system_mapping.py index 8067c12cf..d84e234f7 100644 --- a/inputremapper/configs/system_mapping.py +++ b/inputremapper/configs/system_mapping.py @@ -25,8 +25,8 @@ import evdev -from inputremapper.configs.paths import get_config_path, touch -from inputremapper.logger import logger +from inputremapper.configs.paths import PathUtils +from inputremapper.logger.logger import logger from inputremapper.utils import is_service DISABLE_NAME = "disable" @@ -108,8 +108,8 @@ def _use_xmodmap_symbols(self): # Write this stuff into the input-remapper config directory, because # the systemd service won't know the user sessions xmodmap. - path = get_config_path(XMODMAP_FILENAME) - touch(path) + path = PathUtils.get_config_path(XMODMAP_FILENAME) + PathUtils.touch(path) with open(path, "w") as file: logger.debug('Writing "%s"', path) json.dump(xmodmap_dict, file, indent=4) diff --git a/inputremapper/configs/validation_errors.py b/inputremapper/configs/validation_errors.py index 238256c05..e4d7ce94b 100644 --- a/inputremapper/configs/validation_errors.py +++ b/inputremapper/configs/validation_errors.py @@ -32,7 +32,7 @@ from evdev.ecodes import EV_KEY from inputremapper.configs.system_mapping import system_mapping -from inputremapper.injection.global_uinputs import find_fitting_default_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs class OutputSymbolVariantError(ValueError): @@ -64,7 +64,7 @@ class SymbolNotAvailableInTargetError(ValueError): def __init__(self, symbol, target): code = system_mapping.get(symbol) - fitting_targets = find_fitting_default_uinputs(EV_KEY, code) + fitting_targets = GlobalUInputs.find_fitting_default_uinputs(EV_KEY, code) fitting_targets_string = '", "'.join(fitting_targets) super().__init__( diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index 8d9167c5c..b738984ba 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -38,13 +38,14 @@ gi.require_version("GLib", "2.0") from gi.repository import GLib -from inputremapper.logger import logger, is_debug +from inputremapper.logger.logger import logger from inputremapper.injection.injector import Injector, InjectorState from inputremapper.configs.preset import Preset from inputremapper.configs.global_config import global_config from inputremapper.configs.system_mapping import system_mapping from inputremapper.groups import groups -from inputremapper.configs.paths import get_config_path, sanitize_path_component, USER +from inputremapper.configs.paths import PathUtils +from inputremapper.user import UserUtils from inputremapper.injection.macros.macro import macro_variables from inputremapper.injection.global_uinputs import global_uinputs @@ -190,8 +191,8 @@ def __init__(self): self.config_dir = None - if USER != "root": - self.set_config_dir(get_config_path()) + if UserUtils.home != "root": + self.set_config_dir(PathUtils.get_config_path()) # check privileges if os.getuid() != 0: @@ -227,7 +228,7 @@ def connect(cls, fallback: bool = True) -> DaemonProxy: # Blocks until pkexec is done asking for the password. # Runs via input-remapper-control so that auth_admin_keep works # for all pkexec calls of the gui - debug = " -d" if is_debug() else "" + debug = " -d" if logger.is_debug() else "" cmd = f"pkexec input-remapper-control --command start-daemon {debug}" # using pkexec will also cause the service to continue running in @@ -251,10 +252,10 @@ def connect(cls, fallback: bool = True) -> DaemonProxy: logger.error("Failed to connect to the service") sys.exit(8) - if USER != "root": - config_path = get_config_path() + if UserUtils.home != "root": + config_path = PathUtils.get_config_path() logger.debug('Telling service about "%s"', config_path) - interface.set_config_dir(get_config_path(), timeout=2) + interface.set_config_dir(PathUtils.get_config_path(), timeout=2) return interface @@ -458,7 +459,7 @@ def start_injecting(self, group_key: str, preset_name: str) -> bool: preset_path = PurePath( self.config_dir, "presets", - sanitize_path_component(group.name), + PathUtils.sanitize_path_component(group.name), f"{preset_name}.json", ) diff --git a/inputremapper/evdev/__init__.py b/inputremapper/evdev/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/inputremapper/evdev/evdev.py b/inputremapper/evdev/evdev.py new file mode 100644 index 000000000..12d948568 --- /dev/null +++ b/inputremapper/evdev/evdev.py @@ -0,0 +1,25 @@ +import evdev + + +class Evdev: + # A Wrapper for evdev for improved testability, easily allowing all sorts of crazy + # mocks independent of how evdev is imported, and allowing tiny fixes if something + # in evdev goes wrong. Eventually everything should follow the DI pattern, for even + # better testability by allowing to swap the whole Evdev class out with a mock + # class. + + @staticmethod + def list_devices(*args, **kwargs): + return evdev.list_devices(*args, **kwargs) + + @staticmethod + def input_device_factory(*args, **kwargs): + return evdev.InputDevice(*args, **kwargs) + + @staticmethod + def uinput_factory(*args, **kwargs): + return evdev.UInput(*args, **kwargs) + + @staticmethod + def input_event_factory(*args, **kwargs): + return evdev.InputEvent(*args, **kwargs) diff --git a/inputremapper/groups.py b/inputremapper/groups.py index 615f1e489..76ead984f 100644 --- a/inputremapper/groups.py +++ b/inputremapper/groups.py @@ -37,11 +37,9 @@ import os import re import threading -import traceback from typing import List, Optional import evdev -from evdev import InputDevice from evdev.ecodes import ( EV_KEY, EV_ABS, @@ -56,8 +54,8 @@ REL_WHEEL, ) -from inputremapper.configs.paths import get_preset_path -from inputremapper.logger import logger +from inputremapper.configs.paths import PathUtils +from inputremapper.logger.logger import logger from inputremapper.utils import get_device_hash TABLET_KEYS = [ @@ -197,7 +195,7 @@ def classify(device) -> DeviceType: DENYLIST = [".*Yubico.*YubiKey.*", "Eee PC WMI hotkeys"] -def is_denylisted(device: InputDevice): +def is_denylisted(device: evdev.InputDevice): """Check if a device should not be used in input-remapper. Parameters @@ -211,7 +209,7 @@ def is_denylisted(device: InputDevice): return False -def get_unique_key(device: InputDevice): +def get_unique_key(device: evdev.InputDevice): """Find a string key that is unique for a single hardware device. All InputDevices in /dev/input that originate from the same physical @@ -299,7 +297,7 @@ def get_preset_path(self, preset: Optional[str] = None): This path is unique per device-model, not per group. Groups of the same model share the same preset paths. """ - return get_preset_path(self.name, preset) + return PathUtils.get_preset_path(self.name, preset) def get_devices(self) -> List[evdev.InputDevice]: devices: List[evdev.InputDevice] = [] diff --git a/inputremapper/gui/autocompletion.py b/inputremapper/gui/autocompletion.py index 6d775ce69..c7e8feb85 100644 --- a/inputremapper/gui/autocompletion.py +++ b/inputremapper/gui/autocompletion.py @@ -39,7 +39,7 @@ get_macro_argument_names, remove_comments, ) -from inputremapper.logger import logger +from inputremapper.logger.logger import logger # no deprecated shorthand function-names FUNCTION_NAMES = [name for name in TASK_FACTORIES.keys() if len(name) > 1] diff --git a/inputremapper/gui/components/device_groups.py b/inputremapper/gui/components/device_groups.py index d1ef1d8f3..ae73b161d 100644 --- a/inputremapper/gui/components/device_groups.py +++ b/inputremapper/gui/components/device_groups.py @@ -36,7 +36,7 @@ GroupData, DoStackSwitch, ) -from inputremapper.logger import logger +from inputremapper.logger.logger import logger class DeviceGroupEntry(FlowBoxEntry): diff --git a/inputremapper/gui/components/presets.py b/inputremapper/gui/components/presets.py index d552194f1..46c0644d7 100644 --- a/inputremapper/gui/components/presets.py +++ b/inputremapper/gui/components/presets.py @@ -37,7 +37,7 @@ PresetData, DoStackSwitch, ) -from inputremapper.logger import logger +from inputremapper.logger.logger import logger class PresetEntry(FlowBoxEntry): diff --git a/inputremapper/gui/controller.py b/inputremapper/gui/controller.py index fd854be8b..d44c4d572 100644 --- a/inputremapper/gui/controller.py +++ b/inputremapper/gui/controller.py @@ -47,7 +47,7 @@ MissingMacroOrKeyError, OutputSymbolVariantError, ) -from inputremapper.configs.paths import sanitize_path_component +from inputremapper.configs.paths import PathUtils from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.validation_errors import pydantify from inputremapper.exceptions import DataManagementError @@ -69,7 +69,7 @@ InjectorState, InjectorStateMessage, ) -from inputremapper.logger import logger +from inputremapper.logger.logger import logger if TYPE_CHECKING: # avoids gtk import error in tests @@ -494,7 +494,7 @@ def rename_preset(self, new_name: str): ): return - new_name = sanitize_path_component(new_name) + new_name = PathUtils.sanitize_path_component(new_name) new_name = self.data_manager.get_available_preset_name(new_name) self.data_manager.rename_preset(new_name) diff --git a/inputremapper/gui/data_manager.py b/inputremapper/gui/data_manager.py index fa08399c9..29bfeb171 100644 --- a/inputremapper/gui/data_manager.py +++ b/inputremapper/gui/data_manager.py @@ -28,7 +28,7 @@ from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.mapping import UIMapping, MappingData -from inputremapper.configs.paths import get_preset_path, mkdir, split_all +from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.configs.system_mapping import SystemMapping from inputremapper.daemon import DaemonProxy @@ -51,7 +51,7 @@ InjectorState, InjectorStateMessage, ) -from inputremapper.logger import logger +from inputremapper.logger.logger import logger DEFAULT_PRESET_NAME = _("new preset") @@ -185,8 +185,8 @@ def get_preset_names(self) -> Tuple[Name, ...]: """Get all preset names for active_group and current user sorted by age.""" if not self.active_group: raise DataManagementError("Cannot find presets: Group is not set") - device_folder = get_preset_path(self.active_group.name) - mkdir(device_folder) + device_folder = PathUtils.get_preset_path(self.active_group.name) + PathUtils.mkdir(device_folder) paths = glob.glob(os.path.join(device_folder, "*.json")) presets = [ @@ -232,15 +232,15 @@ def set_autoload(self, status: bool): def get_newest_group_key(self) -> GroupKey: """group_key of the group with the most recently modified preset.""" paths = [] - for path in glob.glob(os.path.join(get_preset_path(), "*/*.json")): - if self._reader_client.groups.find(key=split_all(path)[-2]): + for path in glob.glob(os.path.join(PathUtils.get_preset_path(), "*/*.json")): + if self._reader_client.groups.find(key=PathUtils.split_all(path)[-2]): paths.append((path, os.path.getmtime(path))) if not paths: raise FileNotFoundError() path, _ = max(paths, key=lambda x: x[1]) - return split_all(path)[-2] + return PathUtils.split_all(path)[-2] def get_newest_preset_name(self) -> Name: """Preset name of the most recently modified preset in the active group.""" @@ -250,7 +250,9 @@ def get_newest_preset_name(self) -> Name: paths = [ (path, os.path.getmtime(path)) for path in glob.glob( - os.path.join(get_preset_path(self.active_group.name), "*.json") + os.path.join( + PathUtils.get_preset_path(self.active_group.name), "*.json" + ) ) ] if not paths: @@ -267,7 +269,7 @@ def get_available_preset_name(self, name=DEFAULT_PRESET_NAME) -> Name: name = name.strip() # find a name that is not already taken - if os.path.exists(get_preset_path(self.active_group.name, name)): + if os.path.exists(PathUtils.get_preset_path(self.active_group.name, name)): # if there already is a trailing number, increment it instead of # adding another one match = re.match(r"^(.+) (\d+)$", name) @@ -278,7 +280,7 @@ def get_available_preset_name(self, name=DEFAULT_PRESET_NAME) -> Name: i = 2 while os.path.exists( - get_preset_path(self.active_group.name, f"{name} {i}") + PathUtils.get_preset_path(self.active_group.name, f"{name} {i}") ): i += 1 @@ -314,7 +316,7 @@ def load_preset(self, name: str): logger.info('Loading preset "%s"', name) - preset_path = get_preset_path(self.active_group.name, name) + preset_path = PathUtils.get_preset_path(self.active_group.name, name) preset = Preset(preset_path, mapping_factory=UIMapping) preset.load() self._active_input_config = None @@ -363,13 +365,15 @@ def rename_preset(self, new_name: str): if not self.active_preset or not self.active_group: raise DataManagementError("Unable rename preset: Preset is not set") - if self.active_preset.path == get_preset_path(self.active_group.name, new_name): + if self.active_preset.path == PathUtils.get_preset_path( + self.active_group.name, new_name + ): return old_path = self.active_preset.path assert old_path is not None old_name = os.path.basename(old_path).split(".")[0] - new_path = get_preset_path(self.active_group.name, new_name) + new_path = PathUtils.get_preset_path(self.active_group.name, new_name) if os.path.exists(new_path): raise ValueError( f"cannot rename {old_name} to " f"{new_name}, preset already exists" @@ -383,7 +387,9 @@ def rename_preset(self, new_name: str): if self._config.is_autoloaded(self.active_group.key, old_name): self._config.set_autoload_preset(self.active_group.key, new_name) - self.active_preset.path = get_preset_path(self.active_group.name, new_name) + self.active_preset.path = PathUtils.get_preset_path( + self.active_group.name, new_name + ) self.publish_group() self.publish_preset() @@ -396,13 +402,15 @@ def copy_preset(self, name: str): if not self.active_preset or not self.active_group: raise DataManagementError("Unable to copy preset: Preset is not set") - if self.active_preset.path == get_preset_path(self.active_group.name, name): + if self.active_preset.path == PathUtils.get_preset_path( + self.active_group.name, name + ): return if name in self.get_preset_names(): raise ValueError(f"a preset with the name {name} already exits") - new_path = get_preset_path(self.active_group.name, name) + new_path = PathUtils.get_preset_path(self.active_group.name, name) logger.info('Copy "%s" to "%s"', self.active_preset.path, new_path) self.active_preset.path = new_path self.save() @@ -417,7 +425,7 @@ def create_preset(self, name: str): if not self.active_group: raise DataManagementError("Unable to add preset. Group is not set") - path = get_preset_path(self.active_group.name, name) + path = PathUtils.get_preset_path(self.active_group.name, name) if os.path.exists(path): raise DataManagementError("Unable to add preset. Preset exists") diff --git a/inputremapper/gui/messages/message_broker.py b/inputremapper/gui/messages/message_broker.py index 88b425c6d..0c90f241b 100644 --- a/inputremapper/gui/messages/message_broker.py +++ b/inputremapper/gui/messages/message_broker.py @@ -33,7 +33,7 @@ ) from inputremapper.gui.messages.message_types import MessageType -from inputremapper.logger import logger +from inputremapper.logger.logger import logger if TYPE_CHECKING: pass diff --git a/inputremapper/gui/reader_client.py b/inputremapper/gui/reader_client.py index 65a31cf7a..2dae072b4 100644 --- a/inputremapper/gui/reader_client.py +++ b/inputremapper/gui/reader_client.py @@ -53,7 +53,7 @@ from inputremapper.gui.gettext import _ from inputremapper.input_event import InputEvent from inputremapper.ipc.pipe import Pipe -from inputremapper.logger import logger +from inputremapper.logger.logger import logger BLACKLISTED_EVENTS = [(1, evdev.ecodes.BTN_TOOL_DOUBLETAP)] RecordingGenerator = Generator[None, InputEvent, None] diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index f5bb70e75..510936151 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -65,8 +65,8 @@ from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler from inputremapper.input_event import InputEvent, EventActions from inputremapper.ipc.pipe import Pipe -from inputremapper.logger import logger -from inputremapper.user import USER +from inputremapper.logger.logger import logger +from inputremapper.user import UserUtils # received by the reader-service CMD_TERMINATE = "terminate" @@ -82,8 +82,8 @@ def get_pipe_paths(): """Get the path where the pipe can be found.""" return ( - f"/tmp/input-remapper-{USER}/reader-results", - f"/tmp/input-remapper-{USER}/reader-commands", + f"/tmp/input-remapper-{UserUtils.home}/reader-results", + f"/tmp/input-remapper-{UserUtils.home}/reader-commands", ) diff --git a/inputremapper/gui/user_interface.py b/inputremapper/gui/user_interface.py index 0b9b2b50d..54235eae3 100644 --- a/inputremapper/gui/user_interface.py +++ b/inputremapper/gui/user_interface.py @@ -63,7 +63,7 @@ gtk_iteration, ) from inputremapper.injection.injector import InjectorStateMessage -from inputremapper.logger import logger, COMMIT_HASH, VERSION, EVDEV_VERSION +from inputremapper.logger.logger import logger, COMMIT_HASH, VERSION, EVDEV_VERSION from inputremapper.gui.gettext import _ # https://cjenkins.wordpress.com/2012/05/08/use-gtksourceview-widget-in-glade/ diff --git a/inputremapper/gui/utils.py b/inputremapper/gui/utils.py index 58ec81e02..be3ef3ce0 100644 --- a/inputremapper/gui/utils.py +++ b/inputremapper/gui/utils.py @@ -27,7 +27,7 @@ from gi.repository import Gtk, GLib, Gdk -from inputremapper.logger import logger +from inputremapper.logger.logger import logger # status ctx ids diff --git a/inputremapper/injection/context.py b/inputremapper/injection/context.py index b247e1026..11a324332 100644 --- a/inputremapper/injection/context.py +++ b/inputremapper/injection/context.py @@ -38,7 +38,7 @@ parse_mappings, EventPipelines, ) -from inputremapper.logger import logger +from inputremapper.logger.logger import logger class Context: diff --git a/inputremapper/injection/event_reader.py b/inputremapper/injection/event_reader.py index 67e16bc03..33a702be4 100644 --- a/inputremapper/injection/event_reader.py +++ b/inputremapper/injection/event_reader.py @@ -33,7 +33,7 @@ NotifyCallback, ) from inputremapper.input_event import InputEvent -from inputremapper.logger import logger +from inputremapper.logger.logger import logger class Context(Protocol): diff --git a/inputremapper/injection/global_uinputs.py b/inputremapper/injection/global_uinputs.py index 78b33e7fb..931b8e505 100644 --- a/inputremapper/injection/global_uinputs.py +++ b/inputremapper/injection/global_uinputs.py @@ -23,7 +23,7 @@ import inputremapper.exceptions import inputremapper.utils -from inputremapper.logger import logger +from inputremapper.logger.logger import logger MIN_ABS = -(2**15) # -32768 MAX_ABS = 2**15 # 32768 @@ -59,36 +59,24 @@ } -def can_default_uinput_emit(target: str, type_: int, code: int) -> bool: - """Check if the uinput with the target name is capable of the event.""" - capabilities = DEFAULT_UINPUTS.get(target, {}).get(type_) - return capabilities is not None and code in capabilities - - -def find_fitting_default_uinputs(type_: int, code: int) -> List[str]: - """Find the names of default uinputs that are able to emit this event.""" - return [ - uinput - for uinput in DEFAULT_UINPUTS - if code in DEFAULT_UINPUTS[uinput].get(type_, []) - ] - - class UInput(evdev.UInput): + _capabilities_cache: Optional[Dict] = None + def __init__(self, *args, **kwargs): name = kwargs["name"] logger.debug('creating UInput device: "%s"', name) super().__init__(*args, **kwargs) - # this will never change, so we cache it since evdev runs an expensive loop to - # gather the capabilities. (can_emit is called regularly) - self._capabilities_cache = self.capabilities(absinfo=False) - def can_emit(self, event: Tuple[int, int, int]): """Check if an event can be emitted by the UIinput. Wrong events might be injected if the group mappings are wrong, """ + # this will never change, so we cache it since evdev runs an expensive loop to + # gather the capabilities. (can_emit is called regularly) + if self._capabilities_cache is None: + self._capabilities_cache = self.capabilities(absinfo=False) + return event[1] in self._capabilities_cache.get(event[0], []) @@ -117,6 +105,21 @@ def __init__(self): def __iter__(self): return iter(uinput for _, uinput in self.devices.items()) + @staticmethod + def can_default_uinput_emit(target: str, type_: int, code: int) -> bool: + """Check if the uinput with the target name is capable of the event.""" + capabilities = DEFAULT_UINPUTS.get(target, {}).get(type_) + return capabilities is not None and code in capabilities + + @staticmethod + def find_fitting_default_uinputs(type_: int, code: int) -> List[str]: + """Find the names of default uinputs that are able to emit this event.""" + return [ + uinput + for uinput in DEFAULT_UINPUTS + if code in DEFAULT_UINPUTS[uinput].get(type_, []) + ] + def reset(self): self.is_service = inputremapper.utils.is_service() self._uinput_factory = None @@ -127,6 +130,7 @@ def ensure_uinput_factory_set(self): if self._uinput_factory is not None: return + # TODO this should be solved via DI # overwrite global_uinputs.is_service in tests to control this if self.is_service: logger.debug("Creating regular UInputs") @@ -202,4 +206,5 @@ def get_uinput(self, name: str) -> Optional[evdev.UInput]: return self.devices.get(name) +# TODO DI global_uinputs = GlobalUInputs() diff --git a/inputremapper/injection/injector.py b/inputremapper/injection/injector.py index 4ca7470ce..15435c89a 100644 --- a/inputremapper/injection/injector.py +++ b/inputremapper/injection/injector.py @@ -44,7 +44,7 @@ from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader from inputremapper.injection.numlock import set_numlock, is_numlock_on, ensure_numlock -from inputremapper.logger import logger +from inputremapper.logger.logger import logger from inputremapper.utils import get_device_hash CapabilitiesDict = Dict[int, List[int]] diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index 88e5c4a39..4fc389749 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -59,9 +59,9 @@ SymbolNotAvailableInTargetError, MacroParsingError, ) -from inputremapper.injection.global_uinputs import can_default_uinput_emit +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.ipc.shared_dict import SharedDict -from inputremapper.logger import logger +from inputremapper.logger.logger import logger Handler = Callable[[Tuple[int, int, int]], None] MacroTask = Callable[[Handler], Awaitable] @@ -735,7 +735,9 @@ def _type_check_symbol(self, keyname: Union[str, Variable]) -> Union[Variable, i if self.mapping is not None: target = self.mapping.target_uinput - if target is not None and not can_default_uinput_emit(target, EV_KEY, code): + if target is not None and not GlobalUInputs.can_default_uinput_emit( + target, EV_KEY, code + ): raise SymbolNotAvailableInTargetError(symbol, target) return code diff --git a/inputremapper/injection/macros/parse.py b/inputremapper/injection/macros/parse.py index e1cf588a8..acecf30f6 100644 --- a/inputremapper/injection/macros/parse.py +++ b/inputremapper/injection/macros/parse.py @@ -27,7 +27,7 @@ from inputremapper.configs.validation_errors import MacroParsingError from inputremapper.injection.macros.macro import Macro, Variable -from inputremapper.logger import logger +from inputremapper.logger.logger import logger def is_this_a_macro(output: Any): diff --git a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py index 4756ac0ba..51f86de97 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py @@ -33,7 +33,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger import logger +from inputremapper.logger.logger import logger from inputremapper.utils import get_evdev_constant_name diff --git a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py index e2f0873ae..a4a6b863f 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py @@ -49,7 +49,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger import logger +from inputremapper.logger.logger import logger from inputremapper.utils import get_evdev_constant_name diff --git a/inputremapper/injection/mapping_handlers/axis_switch_handler.py b/inputremapper/injection/mapping_handlers/axis_switch_handler.py index 992e3b64f..13cfa4c4c 100644 --- a/inputremapper/injection/mapping_handlers/axis_switch_handler.py +++ b/inputremapper/injection/mapping_handlers/axis_switch_handler.py @@ -31,7 +31,7 @@ ContextProtocol, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger import logger +from inputremapper.logger.logger import logger from inputremapper.utils import get_device_hash diff --git a/inputremapper/injection/mapping_handlers/combination_handler.py b/inputremapper/injection/mapping_handlers/combination_handler.py index 3b9b68c5d..7e8dcc3e3 100644 --- a/inputremapper/injection/mapping_handlers/combination_handler.py +++ b/inputremapper/injection/mapping_handlers/combination_handler.py @@ -31,7 +31,7 @@ HandlerEnums, ) from inputremapper.input_event import InputEvent -from inputremapper.logger import logger +from inputremapper.logger.logger import logger if TYPE_CHECKING: from inputremapper.injection.context import Context diff --git a/inputremapper/injection/mapping_handlers/key_handler.py b/inputremapper/injection/mapping_handlers/key_handler.py index 5ab0f1f6a..493e41987 100644 --- a/inputremapper/injection/mapping_handlers/key_handler.py +++ b/inputremapper/injection/mapping_handlers/key_handler.py @@ -29,7 +29,7 @@ HandlerEnums, ) from inputremapper.input_event import InputEvent -from inputremapper.logger import logger +from inputremapper.logger.logger import logger from inputremapper.utils import get_evdev_constant_name diff --git a/inputremapper/injection/mapping_handlers/macro_handler.py b/inputremapper/injection/mapping_handlers/macro_handler.py index 4f2d6e394..ea4c9f0c1 100644 --- a/inputremapper/injection/mapping_handlers/macro_handler.py +++ b/inputremapper/injection/mapping_handlers/macro_handler.py @@ -31,7 +31,7 @@ HandlerEnums, ) from inputremapper.input_event import InputEvent -from inputremapper.logger import logger +from inputremapper.logger.logger import logger class MacroHandler(MappingHandler): diff --git a/inputremapper/injection/mapping_handlers/mapping_handler.py b/inputremapper/injection/mapping_handlers/mapping_handler.py index e610f7c00..dc3a7a333 100644 --- a/inputremapper/injection/mapping_handlers/mapping_handler.py +++ b/inputremapper/injection/mapping_handlers/mapping_handler.py @@ -69,7 +69,7 @@ from inputremapper.configs.mapping import Mapping from inputremapper.exceptions import MappingParsingError from inputremapper.input_event import InputEvent -from inputremapper.logger import logger +from inputremapper.logger.logger import logger class EventListener(Protocol): diff --git a/inputremapper/injection/mapping_handlers/mapping_parser.py b/inputremapper/injection/mapping_handlers/mapping_parser.py index 3bc3fa1e7..333021acb 100644 --- a/inputremapper/injection/mapping_handlers/mapping_parser.py +++ b/inputremapper/injection/mapping_handlers/mapping_parser.py @@ -52,7 +52,7 @@ from inputremapper.injection.mapping_handlers.rel_to_abs_handler import RelToAbsHandler from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler from inputremapper.injection.mapping_handlers.rel_to_rel_handler import RelToRelHandler -from inputremapper.logger import logger +from inputremapper.logger.logger import logger from inputremapper.utils import get_evdev_constant_name EventPipelines = Dict[InputConfig, Set[InputEventHandler]] diff --git a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py index 5d670ec2f..945439c94 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py @@ -47,7 +47,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger import logger +from inputremapper.logger.logger import logger class RelToAbsHandler(MappingHandler): diff --git a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py index 1f9bf1453..42aed1257 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py @@ -30,7 +30,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger import logger +from inputremapper.logger.logger import logger class RelToBtnHandler(MappingHandler): diff --git a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py index 98c5d6be0..c9837ac9b 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py @@ -45,7 +45,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent -from inputremapper.logger import logger +from inputremapper.logger.logger import logger def is_wheel(event) -> bool: diff --git a/inputremapper/injection/numlock.py b/inputremapper/injection/numlock.py index 4006cb280..8588a6773 100644 --- a/inputremapper/injection/numlock.py +++ b/inputremapper/injection/numlock.py @@ -28,7 +28,7 @@ import re import subprocess -from inputremapper.logger import logger +from inputremapper.logger.logger import logger def is_numlock_on(): diff --git a/inputremapper/input_event.py b/inputremapper/input_event.py index 6d3e7ad5d..a3f013302 100644 --- a/inputremapper/input_event.py +++ b/inputremapper/input_event.py @@ -41,21 +41,6 @@ class EventActions(enum.Enum): negative_trigger = enum.auto() # original event was negative direction -def validate_event(event): - """Test if the event is valid.""" - if not isinstance(event.type, int): - raise TypeError(f"Expected type to be an int, but got {event.type}") - - if not isinstance(event.code, int): - raise TypeError(f"Expected code to be an int, but got {event.code}") - - if not isinstance(event.value, int): - # this happened to me because I screwed stuff up - raise TypeError(f"Expected value to be an int, but got {event.value}") - - return event - - # Todo: add slots=True as soon as python 3.10 is in common distros @dataclass(frozen=True) class InputEvent: @@ -81,6 +66,21 @@ def __eq__(self, other: InputEvent | evdev.InputEvent | Tuple[int, int, int]): return self.event_tuple == other raise TypeError(f"cannot compare {type(other)} with InputEvent") + @staticmethod + def validate_event(event): + """Test if the event is valid.""" + if not isinstance(event.type, int): + raise TypeError(f"Expected type to be an int, but got {event.type}") + + if not isinstance(event.code, int): + raise TypeError(f"Expected code to be an int, but got {event.code}") + + if not isinstance(event.value, int): + # this happened to me because I screwed stuff up + raise TypeError(f"Expected value to be an int, but got {event.value}") + + return event + @property def input_match_hash(self) -> Hashable: """a Hashable object which is intended to match the InputEvent with a @@ -119,7 +119,7 @@ def from_tuple( f"failed to create InputEvent {event_tuple = } must have length 3" ) - return validate_event( + return cls.validate_event( cls( 0, 0, @@ -133,7 +133,7 @@ def from_tuple( @classmethod def abs(cls, code: int, value: int, origin_hash: Optional[str] = None): """Create an abs event, like joystick movements.""" - return validate_event( + return cls.validate_event( cls( 0, 0, @@ -147,7 +147,7 @@ def abs(cls, code: int, value: int, origin_hash: Optional[str] = None): @classmethod def rel(cls, code: int, value: int, origin_hash: Optional[str] = None): """Create a rel event, like mouse movements.""" - return validate_event( + return cls.validate_event( cls( 0, 0, @@ -164,7 +164,7 @@ def key(cls, code: int, value: Literal[0, 1], origin_hash: Optional[str] = None) A value of 1 means "press", a value of 0 means "release". """ - return validate_event( + return cls.validate_event( cls( 0, 0, diff --git a/inputremapper/ipc/pipe.py b/inputremapper/ipc/pipe.py index 74509bd5d..a56ef5516 100644 --- a/inputremapper/ipc/pipe.py +++ b/inputremapper/ipc/pipe.py @@ -41,8 +41,8 @@ import time from typing import Optional, AsyncIterator, Union -from inputremapper.configs.paths import mkdir, chown -from inputremapper.logger import logger +from inputremapper.configs.paths import PathUtils +from inputremapper.logger.logger import logger class Pipe: @@ -64,7 +64,7 @@ def __init__(self, path): paths = (f"{path}r", f"{path}w") - mkdir(os.path.dirname(path)) + PathUtils.mkdir(os.path.dirname(path)) if not os.path.exists(paths[0]): logger.debug('Creating new pipe for "%s"', path) @@ -77,8 +77,8 @@ def __init__(self, path): self._fds = os.pipe() fds_dir = f"/proc/{os.getpid()}/fd/" - chown(f"{fds_dir}{self._fds[0]}") - chown(f"{fds_dir}{self._fds[1]}") + PathUtils.chown(f"{fds_dir}{self._fds[0]}") + PathUtils.chown(f"{fds_dir}{self._fds[1]}") # to make it accessible by path constants, create symlinks os.symlink(f"{fds_dir}{self._fds[0]}", paths[0]) diff --git a/inputremapper/ipc/shared_dict.py b/inputremapper/ipc/shared_dict.py index 9f57829b2..66b030f89 100644 --- a/inputremapper/ipc/shared_dict.py +++ b/inputremapper/ipc/shared_dict.py @@ -26,7 +26,7 @@ import select from typing import Optional, Any -from inputremapper.logger import logger +from inputremapper.logger.logger import logger class SharedDict: diff --git a/inputremapper/ipc/socket.py b/inputremapper/ipc/socket.py index 3a0510cd8..f6973c0e4 100644 --- a/inputremapper/ipc/socket.py +++ b/inputremapper/ipc/socket.py @@ -57,8 +57,8 @@ import time from typing import Union -from inputremapper.configs.paths import mkdir, chown -from inputremapper.logger import logger +from inputremapper.configs.paths import PathUtils +from inputremapper.logger.logger import logger # something funny that most likely won't appear in messages. # also add some ones so that 01 in the payload won't offset @@ -82,7 +82,7 @@ def __init__(self, path): self._path = path self._unread = [] self.unsent = [] - mkdir(os.path.dirname(path)) + PathUtils.mkdir(os.path.dirname(path)) self.connection = None self.socket = None self._created_at = 0 @@ -261,7 +261,7 @@ def connect(self): _socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) _socket.bind(self._path) _socket.listen(1) - chown(self._path) + PathUtils.chown(self._path) logger.debug('Created socket: "%s"', self._path) self.socket = _socket self.socket.setblocking(False) diff --git a/inputremapper/logger.py b/inputremapper/logger.py deleted file mode 100644 index 5e04c6220..000000000 --- a/inputremapper/logger.py +++ /dev/null @@ -1,336 +0,0 @@ -# -*- coding: utf-8 -*- -# input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb -# -# This file is part of input-remapper. -# -# input-remapper is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# input-remapper is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with input-remapper. If not, see . - -"""Logging setup for input-remapper.""" - -import logging -import os -import sys -import time -from datetime import datetime -from typing import cast - -try: - from inputremapper.commit_hash import COMMIT_HASH -except ImportError: - COMMIT_HASH = "" - - -start = time.time() - -previous_key_debug_log = None -previous_write_debug_log = None - - -def parse_mapping_handler(mapping_handler): - indent = 0 - lines_and_indent = [] - while True: - if isinstance(handler, str): - lines_and_indent.append([mapping_handler, indent]) - break - - if isinstance(mapping_handler, list): - for sub_handler in mapping_handler: - sub_list = parse_mapping_handler(sub_handler) - for line in sub_list: - line[1] += indent - lines_and_indent.extend(sub_list) - break - - lines_and_indent.append([repr(mapping_handler), indent]) - try: - mapping_handler = mapping_handler.child - except AttributeError: - break - - indent += 1 - return lines_and_indent - - -class Logger(logging.Logger): - def debug_mapping_handler(self, mapping_handler): - """Parse the structure of a mapping_handler and log it.""" - if not self.isEnabledFor(logging.DEBUG): - return - - lines_and_indent = parse_mapping_handler(mapping_handler) - for line in lines_and_indent: - indent = " " - msg = indent * line[1] + line[0] - self._log(logging.DEBUG, msg, args=None) - - def write(self, key, uinput): - """Log that an event is being written - - Parameters - ---------- - key - anything that can be string formatted, but usually a tuple of - (type, code, value) tuples - """ - # pylint: disable=protected-access - if not self.isEnabledFor(logging.DEBUG): - return - - global previous_write_debug_log - - str_key = repr(key) - str_key = str_key.replace(",)", ")") - - msg = f'Writing {str_key} to "{uinput.name}"' - - if msg == previous_write_debug_log: - # avoid some super spam from EV_ABS events - return - - previous_write_debug_log = msg - - self._log(logging.DEBUG, msg, args=None) - - -# https://github.com/python/typeshed/issues/1801 -logging.setLoggerClass(Logger) -logger = cast(Logger, logging.getLogger("input-remapper")) - - -def is_debug(): - """True, if the logger is currently in DEBUG or DEBUG mode.""" - return logger.level <= logging.DEBUG - - -class ColorfulFormatter(logging.Formatter): - """Overwritten Formatter to print nicer logs. - - It colors all logs from the same filename in the same color to visually group them - together. It also adds process name, process id, file, line-number and time. - - If debug mode is not active, it will not do any of this. - """ - - def __init__(self): - super().__init__() - - self.file_color_mapping = {} - - # see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit - self.allowed_colors = [] - for r in range(0, 6): - for g in range(0, 6): - for b in range(0, 6): - # https://stackoverflow.com/a/596243 - brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b - if brightness < 1: - # prefer light colors, because most people have a dark - # terminal background - continue - - if g + b <= 1: - # red makes it look like it's an error - continue - - if abs(g - b) < 2 and abs(b - r) < 2 and abs(r - g) < 2: - # no colors that are too grey - continue - - self.allowed_colors.append(self._get_ansi_code(r, g, b)) - - self.level_based_colors = { - logging.WARNING: 11, - logging.ERROR: 9, - logging.FATAL: 9, - } - - def _get_ansi_code(self, r: int, g: int, b: int): - return 16 + b + (6 * g) + (36 * r) - - def _word_to_color(self, word: str): - """Convert a word to a 8bit ansi color code.""" - digit_sum = sum([ord(char) for char in word]) - index = digit_sum % len(self.allowed_colors) - return self.allowed_colors[index] - - def _allocate_debug_log_color(self, record: logging.LogRecord): - """Get the color that represents the source file of the log.""" - if self.file_color_mapping.get(record.filename) is not None: - return self.file_color_mapping[record.filename] - - color = self._word_to_color(record.filename) - - if self.file_color_mapping.get(record.filename) is None: - # calculate the color for each file only once - self.file_color_mapping[record.filename] = color - - return color - - def _get_process_name(self): - """Generate a beaitiful to read name for this process.""" - process_path = sys.argv[0] - process_name = process_path.split("/")[-1] - - if "input-remapper-" in process_name: - process_name = process_name.replace("input-remapper-", "") - - if process_name == "gtk": - process_name = "GUI" - - return process_name - - def _get_format(self, record: logging.LogRecord): - """Generate a message format string.""" - debug_mode = is_debug() - - if record.levelno == logging.INFO and not debug_mode: - # if not launched with --debug, then don't print "INFO:" - return "%(message)s" - - if not debug_mode: - color = self.level_based_colors[record.levelno] - return f"\033[38;5;{color}m%(levelname)s\033[0m: %(message)s" - - color = self._allocate_debug_log_color(record) - if record.levelno in [logging.ERROR, logging.WARNING, logging.FATAL]: - # underline - style = f"\033[4;38;5;{color}m" - else: - style = f"\033[38;5;{color}m" - - process_color = self._word_to_color(f"{os.getpid()}{sys.argv[0]}") - - return ( # noqa - f'{datetime.now().strftime("%H:%M:%S.%f")} ' - f"\033[38;5;{process_color}m" # color - f"{os.getpid()} " - f"{self._get_process_name()} " - "\033[0m" # end style - f"{style}" - f"%(levelname)s " - f"%(filename)s:%(lineno)d: " - "%(message)s" - "\033[0m" # end style - ).replace(" ", " ") - - def format(self, record: logging.LogRecord): - """Overwritten format function.""" - # pylint: disable=protected-access - self._style._fmt = self._get_format(record) - return super().format(record) - - -handler = logging.StreamHandler() -handler.setFormatter(ColorfulFormatter()) -logger.addHandler(handler) -logger.setLevel(logging.INFO) -logging.getLogger("asyncio").setLevel(logging.WARNING) - - -# using pkg_resources to figure out the version fails in many cases, -# so we hardcode it instead -VERSION = "2.0.1" -EVDEV_VERSION = None -try: - # pkg_resources very commonly fails/breaks - import pkg_resources - - EVDEV_VERSION = pkg_resources.require("evdev")[0].version -except Exception as error: - # there have been pkg_resources.DistributionNotFound and - # pkg_resources.ContextualVersionConflict errors so far. - # We can safely ignore all Exceptions here - logger.info("Could not figure out the version") - logger.debug(error) - -# check if the version is something like 1.5.0-beta or 1.5.0-beta.5 -IS_BETA = "beta" in VERSION - - -def log_info(name="input-remapper"): - """Log version and name to the console.""" - logger.info( - "%s %s %s https://github.com/sezanzeb/input-remapper", - name, - VERSION, - COMMIT_HASH, - ) - - if EVDEV_VERSION: - logger.info("python-evdev %s", EVDEV_VERSION) - - if is_debug(): - logger.warning( - "Debug level will log all your keystrokes! Do not post this " - "output in the internet if you typed in sensitive or private " - "information with your device!" - ) - - -def update_verbosity(debug): - """Set the logging verbosity according to the settings object. - - Also enable rich tracebacks in debug mode. - """ - # pylint really doesn't like what I'm doing with rich.traceback here - # pylint: disable=broad-except,import-error,import-outside-toplevel - if debug: - logger.setLevel(logging.DEBUG) - - try: - from rich.traceback import install - - install(show_locals=True) - logger.debug("Using rich.traceback") - except Exception as error: - # since this is optional, just skip all exceptions - if not isinstance(error, ImportError): - logger.debug("Cannot use rich.traceback: %s", error) - else: - logger.setLevel(logging.INFO) - - -def trim_logfile(log_path): - """Keep the logfile short.""" - if not os.path.exists(log_path): - return - - file_size_mb = os.path.getsize(log_path) / 1000 / 1000 - if file_size_mb > 100: - # something went terribly wrong here. The service might timeout because - # it takes too long to trim this file. delete it instead. This probably - # only happens when doing funny things while in debug mode. - logger.warning( - "Removing enormous log file of %dMB", - file_size_mb, - ) - os.remove(log_path) - return - - # the logfile should not be too long to avoid overflowing the storage - try: - with open(log_path, "rb") as file: - binary = file.readlines()[-1000:] - content = [line.decode("utf-8", errors="ignore") for line in binary] - - with open(log_path, "w") as file: - file.truncate(0) - file.writelines(content) - except PermissionError: - # let the outermost PermissionError handler handle it - raise - except Exception as exception: - logger.error('Failed to trim logfile: "%s"', str(exception)) diff --git a/inputremapper/logger/__init__.py b/inputremapper/logger/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/inputremapper/logger/formatter.py b/inputremapper/logger/formatter.py new file mode 100644 index 000000000..a11aa4582 --- /dev/null +++ b/inputremapper/logger/formatter.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2023 sezanzeb +# +# This file is part of input-remapper. +# +# input-remapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# input-remapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with input-remapper. If not, see . + +"""Logging setup for input-remapper.""" + +import logging +import os +import sys +from datetime import datetime + + +class ColorfulFormatter(logging.Formatter): + """Overwritten Formatter to print nicer logs. + + It colors all logs from the same filename in the same color to visually group them + together. It also adds process name, process id, file, line-number and time. + + If debug mode is not active, it will not do any of this. + """ + + def __init__(self, debug_mode: bool = False): + super().__init__() + + self.debug_mode = debug_mode + self.file_color_mapping = {} + + # see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + self.allowed_colors = [] + for r in range(0, 6): + for g in range(0, 6): + for b in range(0, 6): + # https://stackoverflow.com/a/596243 + brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b + if brightness < 1: + # prefer light colors, because most people have a dark + # terminal background + continue + + if g + b <= 1: + # red makes it look like it's an error + continue + + if abs(g - b) < 2 and abs(b - r) < 2 and abs(r - g) < 2: + # no colors that are too grey + continue + + self.allowed_colors.append(self._get_ansi_code(r, g, b)) + + self.level_based_colors = { + logging.WARNING: 11, + logging.ERROR: 9, + logging.FATAL: 9, + } + + def _get_ansi_code(self, r: int, g: int, b: int): + return 16 + b + (6 * g) + (36 * r) + + def _word_to_color(self, word: str): + """Convert a word to a 8bit ansi color code.""" + digit_sum = sum([ord(char) for char in word]) + index = digit_sum % len(self.allowed_colors) + return self.allowed_colors[index] + + def _allocate_debug_log_color(self, record: logging.LogRecord): + """Get the color that represents the source file of the log.""" + if self.file_color_mapping.get(record.filename) is not None: + return self.file_color_mapping[record.filename] + + color = self._word_to_color(record.filename) + + if self.file_color_mapping.get(record.filename) is None: + # calculate the color for each file only once + self.file_color_mapping[record.filename] = color + + return color + + def _get_process_name(self): + """Generate a beaitiful to read name for this process.""" + process_path = sys.argv[0] + process_name = process_path.split("/")[-1] + + if "input-remapper-" in process_name: + process_name = process_name.replace("input-remapper-", "") + + if process_name == "gtk": + process_name = "GUI" + + return process_name + + def _get_format(self, record: logging.LogRecord): + """Generate a message format string.""" + if record.levelno == logging.INFO and not self.debug_mode: + # if not launched with --debug, then don't print "INFO:" + return "%(message)s" + + if not self.debug_mode: + color = self.level_based_colors.get(record.levelno, 9) + return f"\033[38;5;{color}m%(levelname)s\033[0m: %(message)s" + + color = self._allocate_debug_log_color(record) + if record.levelno in [logging.ERROR, logging.WARNING, logging.FATAL]: + # underline + style = f"\033[4;38;5;{color}m" + else: + style = f"\033[38;5;{color}m" + + process_color = self._word_to_color(f"{os.getpid()}{sys.argv[0]}") + + return ( # noqa + f'{datetime.now().strftime("%H:%M:%S.%f")} ' + f"\033[38;5;{process_color}m" # color + f"{os.getpid()} " + f"{self._get_process_name()} " + "\033[0m" # end style + f"{style}" + f"%(levelname)s " + f"%(filename)s:%(lineno)d: " + "%(message)s" + "\033[0m" # end style + ).replace(" ", " ") + + def format(self, record: logging.LogRecord): + """Overwritten format function.""" + # pylint: disable=protected-access + self._style._fmt = self._get_format(record) + return super().format(record) diff --git a/inputremapper/logger/logger.py b/inputremapper/logger/logger.py new file mode 100644 index 000000000..c6f76980d --- /dev/null +++ b/inputremapper/logger/logger.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2023 sezanzeb +# +# This file is part of input-remapper. +# +# input-remapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# input-remapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with input-remapper. If not, see . + +"""Logging setup for input-remapper.""" + +import logging +import time +from typing import cast + +from inputremapper.logger.formatter import ColorfulFormatter + +try: + from inputremapper.commit_hash import COMMIT_HASH +except ImportError: + COMMIT_HASH = "" + + +start = time.time() + +previous_key_debug_log = None +previous_write_debug_log = None + + +class Logger(logging.Logger): + def debug_mapping_handler(self, mapping_handler): + """Parse the structure of a mapping_handler and log it.""" + if not self.isEnabledFor(logging.DEBUG): + return + + lines_and_indent = self._parse_mapping_handler(mapping_handler) + for line in lines_and_indent: + indent = " " + msg = indent * line[1] + line[0] + self._log(logging.DEBUG, msg, args=None) + + def write(self, key, uinput): + """Log that an event is being written + + Parameters + ---------- + key + anything that can be string formatted, but usually a tuple of + (type, code, value) tuples + """ + # pylint: disable=protected-access + if not self.isEnabledFor(logging.DEBUG): + return + + global previous_write_debug_log + + str_key = repr(key) + str_key = str_key.replace(",)", ")") + + msg = f'Writing {str_key} to "{uinput.name}"' + + if msg == previous_write_debug_log: + # avoid some super spam from EV_ABS events + return + + previous_write_debug_log = msg + + self._log(logging.DEBUG, msg, args=None) + + def _parse_mapping_handler(self, mapping_handler): + indent = 0 + lines_and_indent = [] + while True: + if isinstance(mapping_handler, list): + for sub_handler in mapping_handler: + sub_list = self._parse_mapping_handler(sub_handler) + for line in sub_list: + line[1] += indent + lines_and_indent.extend(sub_list) + break + + lines_and_indent.append([repr(mapping_handler), indent]) + try: + mapping_handler = mapping_handler.child + except AttributeError: + break + + indent += 1 + return lines_and_indent + + def is_debug(self): + """True, if the logger is currently in DEBUG mode.""" + return self.level <= logging.DEBUG + + def log_info(self, name="input-remapper"): + """Log version and name to the console.""" + logger.info( + "%s %s %s https://github.com/sezanzeb/input-remapper", + name, + VERSION, + COMMIT_HASH, + ) + + if EVDEV_VERSION: + logger.info("python-evdev %s", EVDEV_VERSION) + + if self.is_debug(): + logger.warning( + "Debug level will log all your keystrokes! Do not post this " + "output in the internet if you typed in sensitive or private " + "information with your device!" + ) + + def update_verbosity(self, debug): + """Set the logging verbosity according to the settings object. + + Also enable rich tracebacks in debug mode. + """ + # pylint really doesn't like what I'm doing with rich.traceback here + # pylint: disable=broad-except,import-error,import-outside-toplevel + if debug: + self.setLevel(logging.DEBUG) + + try: + from rich.traceback import install + + install(show_locals=True) + self.debug("Using rich.traceback") + except Exception as error: + # since this is optional, just skip all exceptions + if not isinstance(error, ImportError): + self.debug("Cannot use rich.traceback: %s", error) + else: + self.setLevel(logging.INFO) + + for handler in self.handlers: + handler.setFormatter(ColorfulFormatter(debug)) + + @classmethod + def bootstrap_logger(cls): + # https://github.com/python/typeshed/issues/1801 + logging.setLoggerClass(cls) + logger = cast(cls, logging.getLogger("input-remapper")) + + handler = logging.StreamHandler() + handler.setFormatter(ColorfulFormatter(False)) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + logging.getLogger("asyncio").setLevel(logging.WARNING) + return logger + + +logger = Logger.bootstrap_logger() + + +# using pkg_resources to figure out the version fails in many cases, +# so we hardcode it instead +VERSION = "2.0.1" +EVDEV_VERSION = None +try: + # pkg_resources very commonly fails/breaks + import pkg_resources + + EVDEV_VERSION = pkg_resources.require("evdev")[0].version +except Exception as error: + # there have been pkg_resources.DistributionNotFound and + # pkg_resources.ContextualVersionConflict errors so far. + # We can safely ignore all Exceptions here + logger.info("Could not figure out the version") + logger.debug(error) + +# check if the version is something like 1.5.0-beta or 1.5.0-beta.5 +IS_BETA = "beta" in VERSION diff --git a/inputremapper/user.py b/inputremapper/user.py index 93669d19e..d9094a7b3 100644 --- a/inputremapper/user.py +++ b/inputremapper/user.py @@ -26,42 +26,46 @@ import pwd -def get_user(): - """Try to find the user who called sudo/pkexec.""" - try: - return os.getlogin() - except OSError: - # failed in some ubuntu installations and in systemd services - pass - - try: - user = os.environ["USER"] - except KeyError: - # possibly the systemd service. no sudo was used - return getpass.getuser() - - if user == "root": +class UserUtils: + @staticmethod + def get_user(): + """Try to find the user who called sudo/pkexec.""" try: - return os.environ["SUDO_USER"] - except KeyError: - # no sudo was used + return os.getlogin() + except OSError: + # failed in some ubuntu installations and in systemd services pass try: - pkexec_uid = int(os.environ["PKEXEC_UID"]) - return pwd.getpwuid(pkexec_uid).pw_name + user = os.environ["USER"] except KeyError: - # no pkexec was used or the uid is unknown - pass - - return user + # possibly the systemd service. no sudo was used + return getpass.getuser() + if user == "root": + try: + return os.environ["SUDO_USER"] + except KeyError: + # no sudo was used + pass -def get_home(user): - """Try to find the user's home directory.""" - return pwd.getpwnam(user).pw_dir + try: + pkexec_uid = int(os.environ["PKEXEC_UID"]) + return pwd.getpwuid(pkexec_uid).pw_name + except KeyError: + # no pkexec was used or the uid is unknown + pass + return user -USER = get_user() + @staticmethod + def get_home(user): + """Try to find the user's home directory.""" + return pwd.getpwnam(user).pw_dir -HOME = get_home(USER) + # An odd construct, but it can't really be helped because this was done as an + # afterthought, after I learned about proper object-oriented software architecture. + # TODO Eventually, UserUtils should be constructed and injected as a UserService, + # which then initializes stuff. + user = get_user() + home = get_home(user) diff --git a/tests/__init__.py b/tests/__init__.py index 6ab9b83bb..e69de29bb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,2 +0,0 @@ -# make sure patches are loaded -import tests.test diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 84869de38..0f61fe4aa 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -86,6 +86,7 @@ ) from inputremapper.configs.mapping import MappingData from inputremapper.configs.input_config import InputCombination, InputConfig +from tests.new_test import setup_tests class ComponentBaseTest(unittest.TestCase): @@ -169,6 +170,7 @@ def get_child_icons(flow_box: Gtk.FlowBox): return icon_names +@setup_tests class TestDeviceGroupSelection(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -233,6 +235,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.load_group.assert_not_called() +@setup_tests class TestTargetSelection(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -280,6 +283,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.update_mapping.assert_not_called() +@setup_tests class TestPresetSelection(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -349,6 +353,7 @@ def test_loads_preset(self): self.controller_mock.load_preset.assert_called_once_with("preset2") +@setup_tests class TestMappingListbox(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -491,6 +496,7 @@ def test_sorts_empty_mapping_to_bottom(self): self.assertEqual(bottom_row.combination, InputCombination.empty_combination()) +@setup_tests class TestMappingSelectionLabel(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -775,6 +781,7 @@ def test_removes_name_when_name_matches_combination(self): self.controller_mock.update_mapping.assert_called_once_with(name="") +@setup_tests class TestGdkEventRecorder(ComponentBaseTest): def _emit_key(self, window, code, type_): event = Gdk.Event() @@ -825,6 +832,7 @@ def test_records_combinations(self): self.assertEqual(label.get_text(), "a") +@setup_tests class TestCodeEditor(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -928,6 +936,7 @@ def unfocus(): self.assertNotIn("opaque-text", self.gui.get_style_context().list_classes()) +@setup_tests class TestRecordingToggle(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -974,6 +983,7 @@ def test_shows_not_recording_when_recording_finished(self): self.assert_not_recording() +@setup_tests class TestStatusBar(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1057,6 +1067,7 @@ def test_sets_msg_as_tooltip_if_tooltip_is_none(self): self.assertEqual(self.get_tooltip(), "msg") +@setup_tests class TestAutoloadSwitch(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1084,6 +1095,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.set_autoload.assert_not_called() +@setup_tests class TestReleaseCombinationSwitch(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1115,6 +1127,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.update_mapping.assert_not_called() +@setup_tests class TestEventEntry(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1135,6 +1148,7 @@ def test_move_event(self): ) +@setup_tests class TestCombinationListbox(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1197,6 +1211,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.load_event.assert_not_called() +@setup_tests class TestAnalogInputSwitch(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1240,6 +1255,7 @@ def test_enables_switch_when_axis_event(self): self.assertTrue(self.gui.get_sensitive()) +@setup_tests class TestTriggerThresholdInput(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1285,6 +1301,7 @@ def test_updates_configuration_according_to_selected_event(self): self.assert_key_event_config() +@setup_tests class TestReleaseTimeoutInput(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1381,6 +1398,7 @@ def test_disables_input_based_on_input_combination(self): self.assertLess(self.gui.get_opacity(), 0.6) +@setup_tests class TestOutputAxisSelector(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1449,6 +1467,7 @@ def test_updates_dropdown_model(self): self.assertEqual(len(self.gui.get_model()), 9) +@setup_tests class TestKeyAxisStackSwitcher(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1511,6 +1530,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.update_mapping.assert_not_called() +@setup_tests class TestTransformationDrawArea(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1548,6 +1568,7 @@ def test_redraws_when_mapping_updates(self): mock.assert_called() +@setup_tests class TestSliders(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1624,6 +1645,7 @@ def test_avoids_recursion(self): self.controller_mock.update_mapping.assert_not_called() +@setup_tests class TestRelativeInputCutoffInput(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1725,6 +1747,7 @@ def test_enables_input(self): self.assert_active() +@setup_tests class TestRequireActiveMapping(ComponentBaseTest): def test_no_reqorded_input_required(self): self.box = Gtk.Box() @@ -1788,6 +1811,7 @@ def assert_active(self, widget: Gtk.Widget): self.assertEqual(widget.get_opacity(), 1) +@setup_tests class TestStack(ComponentBaseTest): def test_switches_pages(self): self.stack = Gtk.Stack() @@ -1807,6 +1831,7 @@ def test_switches_pages(self): self.assertEqual(self.stack.get_visible_child_name(), "Editor") +@setup_tests class TestBreadcrumbs(ComponentBaseTest): def test_breadcrumbs(self): self.label_1 = Gtk.Label() diff --git a/tests/integration/test_daemon.py b/tests/integration/test_daemon.py index 5edad3b58..13507b165 100644 --- a/tests/integration/test_daemon.py +++ b/tests/integration/test_daemon.py @@ -17,9 +17,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . - - -from tests.test import is_service_running +from tests.test import is_service_running, setup_tests import os import multiprocessing @@ -32,6 +30,7 @@ from gi.repository import Gtk from inputremapper.daemon import Daemon, BUS_NAME +from tests.new_test import setup_tests def gtk_iteration(): @@ -40,6 +39,8 @@ def gtk_iteration(): Gtk.main_iteration() +@setup_tests +@setup_tests class TestDBusDaemon(unittest.TestCase): def setUp(self): self.process = multiprocessing.Process( diff --git a/tests/integration/test_data.py b/tests/integration/test_data.py index 08d00d9db..f0f66b5fe 100644 --- a/tests/integration/test_data.py +++ b/tests/integration/test_data.py @@ -24,8 +24,10 @@ import pkg_resources from inputremapper.configs.data import get_data_path +from tests.new_test import setup_tests +@setup_tests class TestData(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index 8224f58cb..57bb39bd3 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -72,7 +72,7 @@ from inputremapper.configs.system_mapping import system_mapping from inputremapper.configs.mapping import Mapping -from inputremapper.configs.paths import CONFIG_PATH, get_preset_path, get_config_path +from inputremapper.configs.paths import PathUtils from inputremapper.configs.global_config import global_config from inputremapper.groups import _Groups from inputremapper.gui.data_manager import DataManager @@ -94,6 +94,7 @@ from inputremapper.injection.injector import InjectorState from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.daemon import Daemon, DaemonProxy +from tests.new_test import setup_tests # iterate a few times when Gtk.main() is called, but don't block @@ -191,6 +192,7 @@ def get_keyval(self): return True, self.keyval +@setup_tests class TestGroupsFromReaderService(unittest.TestCase): def setUp(self): # don't try to connect, return an object instance of it instead @@ -468,6 +470,7 @@ def sleep(self, num_events): gtk_iteration() +@setup_tests class TestColors(GuiTestBase): # requires a running ui, otherwise fails with segmentation faults def test_get_color_falls_back(self): @@ -517,6 +520,7 @@ def test_get_colors(self): self._test_color_wont_fallback(Colors.get_font_color, Colors.fallback_font) +@setup_tests class TestGui(GuiTestBase): """For tests that use the window. @@ -657,7 +661,7 @@ def test_select_device_without_preset(self): # it creates the file for that right away. It may have been possible # to write it such that it doesn't (its empty anyway), but it does, # so use that to test it in more detail. - path = get_preset_path("Bar Device", "new preset") + path = PathUtils.get_preset_path("Bar Device", "new preset") self.assertTrue(os.path.exists(path)) with open(path, "r") as file: self.assertEqual(file.read(), "") @@ -1443,7 +1447,7 @@ def test_rename_and_save(self): self.rename_btn.clicked() gtk_iteration() - preset_path = f"{CONFIG_PATH}/presets/Foo Device/foo.json" + preset_path = f"{PathUtils.config_path()}/presets/Foo Device/foo.json" self.assertTrue(os.path.exists(preset_path)) error_icon = self.user_interface.get("error_status_icon") self.assertFalse(error_icon.get_visible()) @@ -1484,7 +1488,7 @@ def test_check_for_unknown_symbols(self): self.assertFalse(warning_icon.get_visible()) # it will still save it though - with open(get_preset_path("Foo Device", "preset1")) as f: + with open(PathUtils.get_preset_path("Foo Device", "preset1")) as f: content = f.read() self.assertIn("qux", content) self.assertIn("foo", content) @@ -1780,7 +1784,7 @@ def test_start_injecting(self): # correctly uses group.key, not group.name spy2.assert_called_once_with("Foo Device 2", "preset3") - spy1.assert_called_once_with(get_config_path()) + spy1.assert_called_once_with(PathUtils.get_config_path()) for _ in range(10): time.sleep(0.1) @@ -1883,19 +1887,25 @@ def test_stop_injecting(self): def test_delete_preset(self): # as per test_initial_state we already have preset3 loaded - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3")) + ) with PatchedConfirmDelete(self.user_interface, Gtk.ResponseType.CANCEL): self.delete_preset_btn.clicked() gtk_iteration() - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3")) + ) self.assertEqual(self.data_manager.active_preset.name, "preset3") self.assertEqual(self.data_manager.active_group.name, "Foo Device") with PatchedConfirmDelete(self.user_interface): self.delete_preset_btn.clicked() gtk_iteration() - self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset3"))) + self.assertFalse( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3")) + ) self.assertEqual(self.data_manager.active_preset.name, "preset2") self.assertEqual(self.data_manager.active_group.name, "Foo Device") @@ -1984,14 +1994,14 @@ def test_delete_last_preset(self): self.delete_preset_btn.clicked() # the ui should be clean self.assert_gui_clean() - device_path = f"{CONFIG_PATH}/presets/{self.data_manager.active_group.name}" + device_path = f"{PathUtils.config_path()}/presets/{self.data_manager.active_group.name}" self.assertTrue(os.path.exists(f"{device_path}/new preset.json")) self.delete_preset_btn.clicked() gtk_iteration() # deleting an empty preset als doesn't do weird stuff self.assert_gui_clean() - device_path = f"{CONFIG_PATH}/presets/{self.data_manager.active_group.name}" + device_path = f"{PathUtils.config_path()}/presets/{self.data_manager.active_group.name}" self.assertTrue(os.path.exists(f"{device_path}/new preset.json")) def test_enable_disable_output(self): @@ -2036,6 +2046,7 @@ def test_enable_disable_output(self): self.assertFalse(self.output_box.get_sensitive()) +@setup_tests class TestAutocompletion(GuiTestBase): def press_key(self, keyval): event = Gdk.EventKey() @@ -2249,6 +2260,7 @@ def test_cycling(self): ) +@setup_tests class TestDebounce(unittest.TestCase): def test_debounce(self): calls = 0 diff --git a/tests/integration/test_numlock.py b/tests/integration/test_numlock.py index b3a96ef49..30061d76b 100644 --- a/tests/integration/test_numlock.py +++ b/tests/integration/test_numlock.py @@ -24,8 +24,10 @@ import unittest from inputremapper.injection.numlock import is_numlock_on, set_numlock, ensure_numlock +from tests.new_test import setup_tests +@setup_tests class TestNumlock(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/tests/integration/test_user_interface.py b/tests/integration/test_user_interface.py index 87539bbf3..32fb38fb9 100644 --- a/tests/integration/test_user_interface.py +++ b/tests/integration/test_user_interface.py @@ -16,8 +16,10 @@ from inputremapper.gui.user_interface import UserInterface from inputremapper.configs.mapping import MappingData from inputremapper.configs.input_config import InputCombination, InputConfig +from tests.new_test import setup_tests +@setup_tests class TestUserInterface(unittest.TestCase): def setUp(self) -> None: self.message_broker = MessageBroker() diff --git a/tests/test.py b/tests/legacy_test_setup.py similarity index 76% rename from tests/test.py rename to tests/legacy_test_setup.py index 201078f2c..0247e5eaa 100644 --- a/tests/test.py +++ b/tests/legacy_test_setup.py @@ -24,9 +24,14 @@ import os import sys import tracemalloc +import traceback +traceback.print_stack() + +# TODO don't, the decorator should handle it: tracemalloc.start() +# TODO this won't be required anymore: # ensure nothing has loaded if module := sys.modules.get("inputremapper"): imported = [m for m in module.__dict__ if not m.startswith("__")] @@ -61,6 +66,8 @@ def get_project_root(): raise Exception("Could not find project root") +# TODO no, it should be installed locally so that it can be imported properly if really +# needed, instead of doing hacky sys.path stuff # make sure the "tests" module visible sys.path.append(get_project_root()) if __name__ == "__main__": @@ -74,19 +81,12 @@ def get_project_root(): import unittest import subprocess +# TODO don't, the decorator should handle it: os.environ["UNITTEST"] = "1" from tests.lib.fixtures import fixtures -from tests.lib.pipes import setup_pipe -from tests.lib.patches import ( - patch_paths, - patch_events, - patch_os_system, - patch_check_output, - patch_regrab_timeout, - patch_is_running, - patch_evdev, -) +from tests.lib.pipes import setup_pipe, close_pipe +from tests.lib.patches import apply_all_patches from tests.lib.cleanup import cleanup from tests.lib.logger import update_inputremapper_verbosity @@ -100,15 +100,26 @@ def is_service_running(): return False +# TODO don't, the decorator should handle it: if is_service_running(): # let tests control daemon existance raise Exception("Expected the service not to be running already.") -# make sure those pipes exist before any process (the reader-service) gets forked, -# so that events can be pushed after the fork. -for _fixture in fixtures: - setup_pipe(_fixture) +def create_fixture_pipes(): + # make sure those pipes exist before any process (the reader-service) gets forked, + # so that events can be pushed after the fork. + for _fixture in fixtures: + setup_pipe(_fixture) + + +def remove_fixture_pipes(): + for _fixture in fixtures: + close_pipe(_fixture) + + +# TODO don't, the decorator should handle it: +create_fixture_pipes() # applying patches before importing input-remappers modules is important, otherwise @@ -116,19 +127,23 @@ def is_service_running(): # just-in-time in the test-setup functions instead of globally helps. This way, # it is ensured that the patches on evdev and such are already applied, without having # to take care about ordering the files in a special way. -patch_paths() -patch_evdev() -patch_events() -patch_os_system() -patch_check_output() -patch_regrab_timeout() -patch_is_running() -# patch_warnings() - +# TODO +# 1. The above is why we should wrap everything in classes (including libraries like +# evdev that don't follow that pattern) and then use dependency-injection. Some work +# on this has been done already, but more is needed. +# 2. Apply the patches in beforeEach (or a pytest autouse fixture) +# - By doing those two things, we might get a test-setup that works automatically +# correctly in IDEs. This is horrible. And it has been even more horrible in the past. +# +# TODO don't, the decorator should handle it: +apply_all_patches() +# TODO don't, the decorator should handle it: update_inputremapper_verbosity() +# TODO this shouldn't be needed really. Either use pytest or unittest cli to run +# tests, or IDE tools. def main(): cleanup() # https://docs.python.org/3/library/argparse.html diff --git a/tests/lib/cleanup.py b/tests/lib/cleanup.py index 58af6458d..434094fc9 100644 --- a/tests/lib/cleanup.py +++ b/tests/lib/cleanup.py @@ -27,6 +27,7 @@ import psutil from pickle import UnpicklingError +# TODO on it. You don't need a framework for this by the way: # don't import anything from input_remapper gloablly here, because some files execute # code when imported, which can screw up patches. I wish we had a dependency injection # framework that patches together the dependencies during runtime... @@ -73,13 +74,14 @@ def clear_write_history(): def quick_cleanup(log=True): """Reset the applications state.""" + # TODO no: # Reminder: before patches are applied in test.py, no inputremapper module # may be imported. So tests.lib imports them just-in-time in functions instead. from inputremapper.injection.macros.macro import macro_variables from inputremapper.configs.global_config import global_config from inputremapper.configs.system_mapping import system_mapping from inputremapper.gui.utils import debounce_manager - from inputremapper.configs.paths import get_config_path + from inputremapper.configs.paths import PathUtils from inputremapper.injection.global_uinputs import global_uinputs from tests.lib.global_uinputs import reset_global_uinputs_for_service @@ -125,7 +127,7 @@ def quick_cleanup(log=True): if os.path.exists(tmp): shutil.rmtree(tmp) - global_config.path = os.path.join(get_config_path(), "config.json") + global_config.path = os.path.join(PathUtils.get_config_path(), "config.json") global_config.clear_config() global_config._save_config() diff --git a/tests/lib/fixtures.py b/tests/lib/fixtures.py index 13d5198da..6d46088de 100644 --- a/tests/lib/fixtures.py +++ b/tests/lib/fixtures.py @@ -349,11 +349,11 @@ def prepare_presets(): """ from inputremapper.configs.preset import Preset from inputremapper.configs.mapping import Mapping - from inputremapper.configs.paths import get_config_path, get_preset_path + from inputremapper.configs.paths import PathUtils from inputremapper.configs.global_config import global_config from inputremapper.configs.input_config import InputCombination - preset1 = Preset(get_preset_path("Foo Device", "preset1")) + preset1 = Preset(PathUtils.get_preset_path("Foo Device", "preset1")) preset1.add( Mapping.from_combination( InputCombination.from_tuples((1, 1)), @@ -364,7 +364,7 @@ def prepare_presets(): preset1.save() time.sleep(0.1) - preset2 = Preset(get_preset_path("Foo Device", "preset2")) + preset2 = Preset(PathUtils.get_preset_path("Foo Device", "preset2")) preset2.add(Mapping.from_combination(InputCombination.from_tuples((1, 3)))) preset2.add(Mapping.from_combination(InputCombination.from_tuples((1, 4)))) preset2.save() @@ -372,11 +372,11 @@ def prepare_presets(): # make sure the timestamp of preset 3 is the newest, # so that it will be automatically loaded by the GUI time.sleep(0.1) - preset3 = Preset(get_preset_path("Foo Device", "preset3")) + preset3 = Preset(PathUtils.get_preset_path("Foo Device", "preset3")) preset3.add(Mapping.from_combination(InputCombination.from_tuples((1, 5)))) preset3.save() - with open(get_config_path("config.json"), "w") as file: + with open(PathUtils.get_config_path("config.json"), "w") as file: json.dump({"autoload": {"Foo Device 2": "preset2"}}, file, indent=4) global_config.load_config() diff --git a/tests/lib/logger.py b/tests/lib/logger.py index 3af684489..91518f8de 100644 --- a/tests/lib/logger.py +++ b/tests/lib/logger.py @@ -37,9 +37,9 @@ def update_inputremapper_verbosity(): - from inputremapper.logger import update_verbosity + from inputremapper.logger.logger import logger - update_verbosity(True) + logger.update_verbosity(True) def warn_with_traceback(message, category, filename, lineno, file=None, line=None): diff --git a/tests/lib/patches.py b/tests/lib/patches.py index 4213de695..47ae6412c 100644 --- a/tests/lib/patches.py +++ b/tests/lib/patches.py @@ -26,6 +26,7 @@ import subprocess import time from pickle import UnpicklingError +from unittest.mock import patch import evdev @@ -45,9 +46,9 @@ def patch_paths(): - from inputremapper import user + from inputremapper.user import UserUtils - user.HOME = tmp + return patch.object(UserUtils, "home", tmp) class InputDevice: @@ -256,16 +257,23 @@ def copy(self): self.value, ) - evdev.list_devices = list_devices - evdev.InputDevice = InputDevice - evdev.UInput = UInput - evdev.InputEvent = PatchedInputEvent + return [ + patch.object(evdev, "list_devices", list_devices), + patch.object(evdev, "InputDevice", InputDevice), + patch.object(evdev.UInput, "capabilities", UInput.capabilities), + patch.object(evdev.UInput, "write", UInput.write), + patch.object(evdev.UInput, "syn", UInput.syn), + patch.object(evdev.UInput, "__init__", UInput.__init__), + patch.object(evdev, "InputEvent", PatchedInputEvent), + ] def patch_events(): # improve logging of stuff - evdev.InputEvent.__str__ = lambda self: ( - f"InputEvent{(self.type, self.code, self.value)}" + return patch.object( + evdev.InputEvent, + "__str__", + lambda self: (f"InputEvent{(self.type, self.code, self.value)}"), ) @@ -281,7 +289,7 @@ def system(command): raise Exception("Write patches to avoid running pkexec stuff") return original_system(command) - os.system = system + return patch.object(os, "system", system) def patch_check_output(): @@ -297,14 +305,14 @@ def check_output(command, *args, **kwargs): return xmodmap return original_check_output(command, *args, **kwargs) - subprocess.check_output = check_output + return patch.object(subprocess, "check_output", check_output) def patch_regrab_timeout(): # no need for a high number in tests from inputremapper.injection.injector import Injector - Injector.regrab_timeout = 0.05 + return patch.object(Injector, "regrab_timeout", 0.05) def is_running_patch(): @@ -315,7 +323,7 @@ def is_running_patch(): def patch_is_running(): from inputremapper.gui.reader_service import ReaderService - setattr(ReaderService, "is_running", is_running_patch) + return patch.object(ReaderService, "is_running", is_running_patch) class FakeDaemonProxy: @@ -359,3 +367,35 @@ def autoload_single(self, group_key: str) -> None: def hello(self, out: str) -> str: self.calls["hello"].append(out) return out + + +def create_patches(): + return [ + # TODO I have to wrap evdev for patches to work, because the order of imports + # still matters until then. + *patch_evdev(), + # Those are comfortably wrapped in a class, and are therefore easy to patch + patch_paths(), + patch_regrab_timeout(), + patch_is_running(), + patch_events(), + # Sketchy, they only work because the whole modules are imported, instead of + # importing `check_output` and `system` from the module. + patch_os_system(), + patch_check_output(), + ] + + +def apply_all_patches(): + # TODO in the meantime, until evdev is wrapped, this creates and applies patches in + # the right order + for patch in patch_evdev(): + patch.start() + patch_paths().start() + patch_regrab_timeout().start() + patch_is_running().start() + patch_events().start() + patch_os_system().start() + patch_check_output().start() + + # patch_warnings().start() diff --git a/tests/lib/pipes.py b/tests/lib/pipes.py index bee33494e..42780613b 100644 --- a/tests/lib/pipes.py +++ b/tests/lib/pipes.py @@ -52,6 +52,14 @@ def setup_pipe(fixture: Fixture): pending_events[fixture] = multiprocessing.Pipe() +def close_pipe(fixture: Fixture): + if fixture in pending_events: + pipe1, pipe2 = pending_events[fixture] + pipe1.close() + pipe2.close() + del pending_events[fixture] + + def get_events(): """Get all events written by the injector.""" return uinput_write_history diff --git a/tests/new_test.py b/tests/new_test.py new file mode 100644 index 000000000..9b898182f --- /dev/null +++ b/tests/new_test.py @@ -0,0 +1,130 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2023 sezanzeb +# +# This file is part of input-remapper. +# +# input-remapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# input-remapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with input-remapper. If not, see . + +from __future__ import annotations + +import os +import subprocess +import tracemalloc + +from tests.lib.cleanup import cleanup, quick_cleanup +from tests.lib.fixtures import fixtures +from tests.lib.logger import update_inputremapper_verbosity +from tests.lib.patches import create_patches +from tests.lib.pipes import setup_pipe, close_pipe + + +def get_project_root(): + """Find the projects root, i.e. the uppermost directory of the repo.""" + # when tests are started in pycharm via the green arrow, the working directory + # is not the project root. Go up until it is found. + root = os.getcwd() + for _ in range(10): + if "setup.py" in os.listdir(root): + return root + + root = os.path.dirname(root) + + raise Exception("Could not find project root") + + +def is_service_running(): + """Check if the daemon is running.""" + try: + subprocess.check_output(["pgrep", "-f", "input-remapper-service"]) + return True + except subprocess.CalledProcessError: + return False + + +def create_fixture_pipes(): + # make sure those pipes exist before any process (the reader-service) gets forked, + # so that events can be pushed after the fork. + for _fixture in fixtures: + setup_pipe(_fixture) + + +def remove_fixture_pipes(): + for _fixture in fixtures: + close_pipe(_fixture) + + +def setup_tests(cls): + """A class decorator to + - apply the patches to all tests + - check if the deamon is already running + - create pipes to send events to the reader service + - reset stuff automatically + """ + original_setUp = cls.setUp + original_tearDown = cls.tearDown + original_setUpClass = cls.setUpClass + original_tearDownClass = cls.tearDownClass + + tracemalloc.start() + os.environ["UNITTEST"] = "1" + update_inputremapper_verbosity() + + patches = create_patches() + + def setUpClass(): + if is_service_running(): + # let tests control daemon existance + raise Exception("Expected the service not to be running already.") + + create_fixture_pipes() + + original_setUpClass() + + def tearDownClass(): + original_tearDownClass() + + remove_fixture_pipes() + + # Do the more thorough cleanup after all tests of classes, because it slows + # tests down. If this is required after each test, call it in your tearDown + # method. + cleanup() + + def setUp(self): + for patch in patches: + patch.start() + + original_setUp(self) + + def tearDown(self): + original_tearDown(self) + + # TODO remove quick_cleanup calls from tearDown methods + quick_cleanup() + + for patch in patches: + patch.stop() + + cls.setUp = setUp + cls.tearDown = tearDown + cls.setUpClass = setUpClass + cls.tearDownClass = tearDownClass + + return cls + + +# TODO maybe make a PR for just the test-setup, without PathUtils and UserUtils. +# Then prepare another PR for DI. diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 52b471dd6..8a6637a25 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,3 +1 @@ """Tests that don't require a complete linux desktop.""" - -import tests.test diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 2b0244420..83332d5a0 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -19,16 +19,18 @@ # along with input-remapper. If not, see . -from tests.lib.cleanup import quick_cleanup -from tests.lib.tmp import tmp - import os import unittest from inputremapper.configs.global_config import global_config -from inputremapper.configs.paths import touch +from inputremapper.configs.paths import PathUtils + +from tests.lib.cleanup import quick_cleanup +from tests.lib.tmp import tmp +from tests.new_test import setup_tests +@setup_tests class TestConfig(unittest.TestCase): def tearDown(self): quick_cleanup() @@ -122,7 +124,7 @@ def test_save_load(self): ) config_2 = os.path.join(tmp, "config_2.json") - touch(config_2) + PathUtils.touch(config_2) with open(config_2, "w") as f: f.write('{"a":"b"}') diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index 1e4503ae5..f3e1ed015 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -32,9 +32,11 @@ from inputremapper.injection.context import Context from inputremapper.configs.preset import Preset from inputremapper.configs.mapping import Mapping -from inputremapper.configs.input_config import InputConfig, InputCombination +from inputremapper.configs.input_config import InputCombination +from tests.new_test import setup_tests +@setup_tests class TestContext(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index 0a7fdba5a..2471fe9bf 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -20,7 +20,7 @@ """Testing the input-remapper-control command""" - +from unittest.mock import patch from tests.lib.cleanup import quick_cleanup from tests.lib.tmp import tmp @@ -28,7 +28,6 @@ import os import time import unittest -from unittest import mock import collections from importlib.util import spec_from_loader, module_from_spec from importlib.machinery import SourceFileLoader @@ -36,8 +35,9 @@ from inputremapper.configs.global_config import global_config from inputremapper.daemon import Daemon from inputremapper.configs.preset import Preset -from inputremapper.configs.paths import get_preset_path +from inputremapper.configs.paths import PathUtils from inputremapper.groups import groups +from tests.new_test import setup_tests def import_control(): @@ -53,10 +53,10 @@ def import_control(): module = module_from_spec(spec) spec.loader.exec_module(module) - return module.communicate, module.utils, module.internals + return module.InputRemapperControl, module.Options -communicate, utils, internals = import_control() +InputRemapperControl, Options = import_control() options = collections.namedtuple( @@ -65,7 +65,11 @@ def import_control(): ) +@setup_tests class TestControl(unittest.TestCase): + def setUp(self): + self.input_remapper_control = InputRemapperControl() + def tearDown(self): quick_cleanup() @@ -74,9 +78,9 @@ def test_autoload(self): groups_ = [groups.find(key=key) for key in device_keys] presets = ["bar0", "bar", "bar2"] paths = [ - get_preset_path(groups_[0].name, presets[0]), - get_preset_path(groups_[1].name, presets[1]), - get_preset_path(groups_[1].name, presets[2]), + PathUtils.get_preset_path(groups_[0].name, presets[0]), + PathUtils.get_preset_path(groups_[1].name, presets[1]), + PathUtils.get_preset_path(groups_[1].name, presets[2]), ] Preset(paths[0]).save() @@ -85,6 +89,8 @@ def test_autoload(self): daemon = Daemon() + self.input_remapper_control.set_daemon(daemon) + start_history = [] stop_counter = 0 @@ -99,12 +105,17 @@ def start_injecting(device: str, preset: str): start_history.append((device, preset)) daemon.injectors[device] = Injector() - daemon.start_injecting = start_injecting + patch.object(daemon, "start_injecting", start_injecting).start() global_config.set_autoload_preset(groups_[0].key, presets[0]) global_config.set_autoload_preset(groups_[1].key, presets[1]) - communicate(options("autoload", None, None, None, False, False, False), daemon) + self.input_remapper_control.communicate( + command="autoload", + config_dir=None, + preset=None, + device=None, + ) self.assertEqual(len(start_history), 2) self.assertEqual(start_history[0], (groups_[0].key, presets[0])) self.assertEqual(start_history[1], (groups_[1].key, presets[1])) @@ -118,7 +129,12 @@ def start_injecting(device: str, preset: str): ) # calling autoload again doesn't load redundantly - communicate(options("autoload", None, None, None, False, False, False), daemon) + self.input_remapper_control.communicate( + command="autoload", + config_dir=None, + preset=None, + device=None, + ) self.assertEqual(len(start_history), 2) self.assertEqual(stop_counter, 0) self.assertFalse( @@ -129,9 +145,11 @@ def start_injecting(device: str, preset: str): ) # unless the injection in question ist stopped - communicate( - options("stop", None, None, groups_[0].key, False, False, False), - daemon, + self.input_remapper_control.communicate( + command="stop", + config_dir=None, + preset=None, + device=groups_[0].key, ) self.assertEqual(stop_counter, 1) self.assertTrue( @@ -140,7 +158,12 @@ def start_injecting(device: str, preset: str): self.assertFalse( daemon.autoload_history.may_autoload(groups_[1].key, presets[1]) ) - communicate(options("autoload", None, None, None, False, False, False), daemon) + self.input_remapper_control.communicate( + command="autoload", + config_dir=None, + preset=None, + device=None, + ) self.assertEqual(len(start_history), 3) self.assertEqual(start_history[2], (groups_[0].key, presets[0])) self.assertFalse( @@ -151,7 +174,12 @@ def start_injecting(device: str, preset: str): ) # if a device name is passed, will only start injecting for that one - communicate(options("stop-all", None, None, None, False, False, False), daemon) + self.input_remapper_control.communicate( + command="stop-all", + config_dir=None, + preset=None, + device=None, + ) self.assertTrue( daemon.autoload_history.may_autoload(groups_[0].key, presets[0]) ) @@ -160,9 +188,11 @@ def start_injecting(device: str, preset: str): ) self.assertEqual(stop_counter, 3) global_config.set_autoload_preset(groups_[1].key, presets[2]) - communicate( - options("autoload", None, None, groups_[1].key, False, False, False), - daemon, + self.input_remapper_control.communicate( + command="autoload", + config_dir=None, + preset=None, + device=groups_[1].key, ) self.assertEqual(len(start_history), 4) self.assertEqual(start_history[3], (groups_[1].key, presets[2])) @@ -175,9 +205,11 @@ def start_injecting(device: str, preset: str): # autoloading for the same device again redundantly will not autoload # again - communicate( - options("autoload", None, None, groups_[1].key, False, False, False), - daemon, + self.input_remapper_control.communicate( + command="autoload", + config_dir=None, + preset=None, + device=groups_[1].key, ) self.assertEqual(len(start_history), 4) self.assertEqual(stop_counter, 3) @@ -211,6 +243,7 @@ def test_autoload_other_path(self): Preset(paths[1]).save() daemon = Daemon() + self.input_remapper_control.set_daemon(daemon) start_history = [] daemon.start_injecting = lambda *args: start_history.append(args) @@ -220,9 +253,11 @@ def test_autoload_other_path(self): global_config.set_autoload_preset(device_names[0], presets[0]) global_config.set_autoload_preset(device_names[1], presets[1]) - communicate( - options("autoload", config_dir, None, None, False, False, False), - daemon, + self.input_remapper_control.communicate( + command="autoload", + config_dir=config_dir, + preset=None, + device=None, ) self.assertEqual(len(start_history), 2) @@ -234,6 +269,7 @@ def test_start_stop(self): preset = "preset9" daemon = Daemon() + self.input_remapper_control.set_daemon(daemon) start_history = [] stop_history = [] @@ -242,23 +278,32 @@ def test_start_stop(self): daemon.stop_injecting = lambda *args: stop_history.append(args) daemon.stop_all = lambda *args: stop_all_history.append(args) - communicate( - options("start", None, preset, group.paths[0], False, False, False), - daemon, + self.input_remapper_control.communicate( + command="start", + config_dir=None, + preset=preset, + device=group.paths[0], ) self.assertEqual(len(start_history), 1) self.assertEqual(start_history[0], (group.key, preset)) - communicate( - options("stop", None, None, group.paths[1], False, False, False), - daemon, + self.input_remapper_control.communicate( + command="stop", + config_dir=None, + preset=None, + device=group.paths[1], ) self.assertEqual(len(stop_history), 1) # provided any of the groups paths as --device argument, figures out # the correct group.key to use here self.assertEqual(stop_history[0], (group.key,)) - communicate(options("stop-all", None, None, None, False, False, False), daemon) + self.input_remapper_control.communicate( + command="stop-all", + config_dir=None, + preset=None, + device=None, + ) self.assertEqual(len(stop_all_history), 1) self.assertEqual(stop_all_history[0], ()) @@ -268,17 +313,32 @@ def test_config_not_found(self): config_dir = "/foo/bar" daemon = Daemon() + self.input_remapper_control.set_daemon(daemon) start_history = [] stop_history = [] daemon.start_injecting = lambda *args: start_history.append(args) daemon.stop_injecting = lambda *args: stop_history.append(args) - options_1 = options("start", config_dir, path, key, False, False, False) - self.assertRaises(SystemExit, lambda: communicate(options_1, daemon)) + self.assertRaises( + SystemExit, + lambda: self.input_remapper_control.communicate( + command="start", + config_dir=config_dir, + preset=path, + device=key, + ), + ) - options_2 = options("stop", config_dir, None, key, False, False, False) - self.assertRaises(SystemExit, lambda: communicate(options_2, daemon)) + self.assertRaises( + SystemExit, + lambda: self.input_remapper_control.communicate( + command="stop", + config_dir=config_dir, + preset=None, + device=key, + ), + ) def test_autoload_config_dir(self): daemon = Daemon() @@ -302,19 +362,18 @@ def test_autoload_config_dir(self): daemon.set_config_dir(os.path.join(tmp, "qux")) self.assertEqual(global_config.get("foo"), "bar") - def test_internals(self): - with mock.patch("os.system") as os_system_patch: - internals( - options("start-reader-service", None, None, None, False, False, False) - ) + def test_internals_reader(self): + with patch.object(os, "system") as os_system_patch: + self.input_remapper_control.internals("start-reader-service", False) os_system_patch.assert_called_once() self.assertIn( "input-remapper-reader-service", os_system_patch.call_args.args[0] ) self.assertNotIn("-d", os_system_patch.call_args.args[0]) - with mock.patch("os.system") as os_system_patch: - internals(options("start-daemon", None, None, None, False, False, True)) + def test_internals_daemon(self): + with patch.object(os, "system") as os_system_patch: + self.input_remapper_control.internals("start-daemon", True) os_system_patch.assert_called_once() self.assertIn("input-remapper-service", os_system_patch.call_args.args[0]) self.assertIn("-d", os_system_patch.call_args.args[0]) diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index 6e714f773..8f640d7af 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -52,7 +52,7 @@ from inputremapper.gui.utils import CTX_ERROR, CTX_APPLY, gtk_iteration from inputremapper.gui.gettext import _ from inputremapper.injection.global_uinputs import GlobalUInputs -from inputremapper.configs.mapping import UIMapping, MappingData, MappingType, Mapping +from inputremapper.configs.mapping import UIMapping, MappingData, Mapping from tests.lib.cleanup import quick_cleanup from tests.lib.stuff import spy from tests.lib.patches import FakeDaemonProxy @@ -60,10 +60,12 @@ from inputremapper.configs.global_config import GlobalConfig from inputremapper.gui.controller import Controller, MAPPING_DEFAULTS from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME -from inputremapper.configs.paths import get_preset_path, CONFIG_PATH +from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset +from tests.new_test import setup_tests +@setup_tests class TestController(unittest.TestCase): def setUp(self) -> None: super().setUp() @@ -231,7 +233,7 @@ def f(data): def test_on_load_preset_should_provide_default_mapping(self): """If there is none.""" - Preset(get_preset_path("Foo Device", "bar")).save() + Preset(PathUtils.get_preset_path("Foo Device", "bar")).save() self.data_manager.load_group("Foo Device 2") calls: List[MappingData] = [] @@ -253,7 +255,9 @@ def test_on_delete_preset_asks_for_confirmation(self): def test_deletes_preset_when_confirmed(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") @@ -261,32 +265,46 @@ def test_deletes_preset_when_confirmed(self): MessageType.user_confirm_request, lambda msg: msg.respond(True) ) self.controller.delete_preset() - self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset2"))) + self.assertFalse( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) def test_does_not_delete_preset_when_not_confirmed(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") self.user_interface.confirm_delete.configure_mock( return_value=Gtk.ResponseType.CANCEL ) self.controller.delete_preset() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) def test_copy_preset(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") self.controller.copy_preset() - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy")) + ) def test_copy_preset_should_add_number(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") @@ -294,13 +312,21 @@ def test_copy_preset_should_add_number(self): self.data_manager.load_preset("preset2") self.controller.copy_preset() # creates "preset2 copy 2" - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 2"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 2")) + ) def test_copy_preset_should_increment_existing_number(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") @@ -310,27 +336,45 @@ def test_copy_preset_should_increment_existing_number(self): self.data_manager.load_preset("preset2") self.controller.copy_preset() # creates "preset2 copy 3" - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 3"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 2")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 3")) + ) def test_copy_preset_should_not_append_copy_twice(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") self.controller.copy_preset() # creates "preset2 copy" self.controller.copy_preset() # creates "preset2 copy 2" not "preset2 copy copy" - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 2"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 2")) + ) def test_copy_preset_should_not_append_copy_to_copy_with_number(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") @@ -339,28 +383,42 @@ def test_copy_preset_should_not_append_copy_to_copy_with_number(self): self.controller.copy_preset() # creates "preset2 copy 2" self.controller.copy_preset() # creates "preset2 copy 3" not "preset2 copy 2 copy" - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2 copy 3"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 2")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 3")) + ) def test_rename_preset(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) - self.assertFalse(os.path.exists(get_preset_path("Foo Device", "foo"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertFalse(os.path.exists(PathUtils.get_preset_path("Foo Device", "foo"))) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") self.controller.rename_preset(new_name="foo") - self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "foo"))) + self.assertFalse( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertTrue(os.path.exists(PathUtils.get_preset_path("Foo Device", "foo"))) def test_rename_preset_sanitized(self): - Preset(get_preset_path("Qux/Device?", "bla")).save() + Preset(PathUtils.get_preset_path("Qux/Device?", "bla")).save() - self.assertTrue(os.path.isfile(get_preset_path("Qux/Device?", "bla"))) - self.assertFalse(os.path.exists(get_preset_path("Qux/Device?", "blubb"))) + self.assertTrue(os.path.isfile(PathUtils.get_preset_path("Qux/Device?", "bla"))) + self.assertFalse( + os.path.exists(PathUtils.get_preset_path("Qux/Device?", "blubb")) + ) self.data_manager.load_group("Qux/Device?") self.data_manager.load_preset("bla") @@ -368,70 +426,102 @@ def test_rename_preset_sanitized(self): # all functions expect the true name, which is also shown to the user, but on # the file system it always uses sanitized names. - self.assertTrue(os.path.exists(get_preset_path("Qux/Device?", "foo__bar"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Qux/Device?", "foo__bar")) + ) # since the name is never stored in an un-sanitized way, this can't work - self.assertFalse(os.path.exists(get_preset_path("Qux/Device?", "foo:/bar"))) + self.assertFalse( + os.path.exists(PathUtils.get_preset_path("Qux/Device?", "foo:/bar")) + ) - path = os.path.join(CONFIG_PATH, "presets", "Qux_Device_", "foo__bar.json") + path = os.path.join( + PathUtils.config_path(), "presets", "Qux_Device_", "foo__bar.json" + ) self.assertTrue(os.path.exists(path)) # using the sanitized name in function calls works as well - self.assertTrue(os.path.isfile(get_preset_path("Qux_Device_", "foo__bar"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Qux_Device_", "foo__bar")) + ) def test_rename_preset_should_pick_available_name(self): prepare_presets() - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3"))) - self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset3 2"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3")) + ) + self.assertFalse( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3 2")) + ) self.data_manager.load_group("Foo Device") self.data_manager.load_preset("preset2") self.controller.rename_preset(new_name="preset3") - self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset2"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3"))) - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3 2"))) + self.assertFalse( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3")) + ) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3 2")) + ) def test_rename_preset_should_not_rename_to_empty_name(self): prepare_presets() - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device") self.data_manager.load_preset("preset2") self.controller.rename_preset(new_name="") - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) def test_rename_preset_should_not_update_same_name(self): """When the new name is the same as the current name.""" prepare_presets() - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") self.controller.rename_preset(new_name="preset2") - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset2"))) - self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset2 2"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2")) + ) + self.assertFalse( + os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 2")) + ) def test_on_add_preset_uses_default_name(self): self.assertFalse( - os.path.exists(get_preset_path("Foo Device", DEFAULT_PRESET_NAME)) + os.path.exists(PathUtils.get_preset_path("Foo Device", DEFAULT_PRESET_NAME)) ) self.data_manager.load_group("Foo Device 2") self.controller.add_preset() - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "new preset"))) + self.assertTrue( + os.path.exists(PathUtils.get_preset_path("Foo Device", "new preset")) + ) def test_on_add_preset_uses_provided_name(self): - self.assertFalse(os.path.exists(get_preset_path("Foo Device", "foo"))) + self.assertFalse(os.path.exists(PathUtils.get_preset_path("Foo Device", "foo"))) self.data_manager.load_group("Foo Device 2") self.controller.add_preset(name="foo") - self.assertTrue(os.path.exists(get_preset_path("Foo Device", "foo"))) + self.assertTrue(os.path.exists(PathUtils.get_preset_path("Foo Device", "foo"))) def test_on_add_preset_shows_permission_error_status(self): self.data_manager.load_group("Foo Device 2") @@ -513,7 +603,9 @@ def test_delete_mapping_asks_for_confirmation(self): def test_deletes_mapping_when_confirmed(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") @@ -524,7 +616,7 @@ def test_deletes_mapping_when_confirmed(self): self.controller.delete_mapping() self.controller.save() - preset = Preset(get_preset_path("Foo Device", "preset2")) + preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2")) preset.load() self.assertIsNone( preset.get_mapping(InputCombination([InputConfig(type=1, code=3)])) @@ -532,7 +624,9 @@ def test_deletes_mapping_when_confirmed(self): def test_does_not_delete_mapping_when_not_confirmed(self): prepare_presets() - self.assertTrue(os.path.isfile(get_preset_path("Foo Device", "preset2"))) + self.assertTrue( + os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2")) + ) self.data_manager.load_group("Foo Device 2") self.data_manager.load_preset("preset2") @@ -544,7 +638,7 @@ def test_does_not_delete_mapping_when_not_confirmed(self): self.controller.delete_mapping() self.controller.save() - preset = Preset(get_preset_path("Foo Device", "preset2")) + preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2")) preset.load() self.assertIsNotNone( preset.get_mapping(InputCombination([InputConfig(type=1, code=3)])) diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index 2381a6e55..25b3baa2b 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -20,7 +20,6 @@ from evdev._ecodes import EV_ABS from inputremapper.input_event import InputEvent -from tests.test import is_service_running from tests.lib.logger import logger from tests.lib.cleanup import cleanup from tests.lib.fixtures import Fixture @@ -42,26 +41,27 @@ from inputremapper.configs.mapping import Mapping from inputremapper.configs.global_config import global_config from inputremapper.groups import groups -from inputremapper.configs.paths import get_config_path, mkdir, get_preset_path +from inputremapper.configs.paths import PathUtils from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.preset import Preset from inputremapper.injection.injector import InjectorState from inputremapper.daemon import Daemon from inputremapper.injection.global_uinputs import global_uinputs - +from tests.new_test import setup_tests, is_service_running check_output = subprocess.check_output os_system = os.system dbus_get = type(SystemBus()).get +@setup_tests class TestDaemon(unittest.TestCase): new_fixture_path = "/dev/input/event9876" def setUp(self): self.grab = evdev.InputDevice.grab self.daemon = None - mkdir(get_config_path()) + PathUtils.mkdir(PathUtils.get_config_path()) global_config._save_config() # the daemon should be able to create them on demand: @@ -112,8 +112,8 @@ def set_config_dir(self, *args, **kwargs): def test_daemon(self): # remove the existing system mapping to force our own into it - if os.path.exists(get_config_path("xmodmap.json")): - os.remove(get_config_path("xmodmap.json")) + if os.path.exists(PathUtils.get_config_path("xmodmap.json")): + os.remove(PathUtils.get_config_path("xmodmap.json")) preset_name = "foo" @@ -217,12 +217,12 @@ def test_config_dir(self): # This is important so that if the service is started via sudo or pkexec # it knows where to look for configuration files. self.daemon = Daemon() - self.assertEqual(self.daemon.config_dir, get_config_path()) + self.assertEqual(self.daemon.config_dir, PathUtils.get_config_path()) self.assertIsNone(global_config.get("foo")) def test_refresh_on_start(self): - if os.path.exists(get_config_path("xmodmap.json")): - os.remove(get_config_path("xmodmap.json")) + if os.path.exists(PathUtils.get_config_path("xmodmap.json")): + os.remove(PathUtils.get_config_path("xmodmap.json")) preset_name = "foo" key_code = 9 @@ -238,7 +238,7 @@ def test_refresh_on_start(self): system_mapping.clear() system_mapping._set("a", KEY_A) - preset = Preset(get_preset_path(group_name, preset_name)) + preset = Preset(PathUtils.get_preset_path(group_name, preset_name)) preset.add( Mapping.from_combination( InputCombination([InputConfig(type=EV_KEY, code=key_code)]), @@ -248,7 +248,7 @@ def test_refresh_on_start(self): ) # make the daemon load the file instead - with open(get_config_path("xmodmap.json"), "w") as file: + with open(PathUtils.get_config_path("xmodmap.json"), "w") as file: json.dump(system_mapping._mapping, file, indent=4) system_mapping.clear() diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index 1583f794c..ef3e63b76 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -44,9 +44,10 @@ from tests.lib.patches import FakeDaemonProxy from tests.lib.fixtures import prepare_presets -from inputremapper.configs.paths import get_preset_path +from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME +from tests.new_test import setup_tests class Listener: @@ -57,6 +58,7 @@ def __call__(self, data): self.calls.append(data) +@setup_tests class TestDataManager(unittest.TestCase): def setUp(self) -> None: self.message_broker = MessageBroker() @@ -135,7 +137,7 @@ def test_load_preset(self): mappings = listener.calls[0].mappings preset_name = listener.calls[0].name - expected_preset = Preset(get_preset_path("Foo Device", "preset1")) + expected_preset = Preset(PathUtils.get_preset_path("Foo Device", "preset1")) expected_preset.load() expected_mappings = list(expected_preset) @@ -167,7 +169,7 @@ def test_save_preset(self): ) mapping: MappingData = listener.calls[0] - control_preset = Preset(get_preset_path("Foo Device", "preset1")) + control_preset = Preset(PathUtils.get_preset_path("Foo Device", "preset1")) control_preset.load() self.assertEqual( control_preset.get_mapping( @@ -353,9 +355,9 @@ def test_delete_preset(self): def test_delete_preset_sanitized(self): """should be able to delete the current preset""" - Preset(get_preset_path("Qux/Device?", "bla")).save() - Preset(get_preset_path("Qux/Device?", "foo")).save() - self.assertTrue(os.path.exists(get_preset_path("Qux/Device?", "bla"))) + Preset(PathUtils.get_preset_path("Qux/Device?", "bla")).save() + Preset(PathUtils.get_preset_path("Qux/Device?", "foo")).save() + self.assertTrue(os.path.exists(PathUtils.get_preset_path("Qux/Device?", "bla"))) self.data_manager.load_group(group_key="Qux/Device?") self.data_manager.load_preset(name="bla") @@ -373,7 +375,9 @@ def test_delete_preset_sanitized(self): self.assertIn("foo", presets_in_group) self.assertEqual(len(listener.calls), 1) - self.assertFalse(os.path.exists(get_preset_path("Qux/Device?", "bla"))) + self.assertFalse( + os.path.exists(PathUtils.get_preset_path("Qux/Device?", "bla")) + ) def test_load_mapping(self): """should be able to load a mapping""" @@ -540,7 +544,7 @@ def test_updated_mapping_can_be_saved(self): ) self.data_manager.save() - preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping) + preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2"), UIMapping) preset.load() mapping = preset.get_mapping(InputCombination([InputConfig(type=1, code=4)])) self.assertEqual(mapping.format_name(), "foo") @@ -561,7 +565,7 @@ def test_updated_mapping_saves_invalid_mapping(self): ) self.data_manager.save() - preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping) + preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2"), UIMapping) preset.load() mapping = preset.get_mapping(InputCombination([InputConfig(type=1, code=4)])) self.assertIsNotNone(mapping.get_error()) @@ -667,7 +671,7 @@ def test_delete_mapping(self): """should be able to delete a mapping""" prepare_presets() - old_preset = Preset(get_preset_path("Foo Device", "preset2")) + old_preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2")) old_preset.load() self.data_manager.load_group(group_key="Foo Device 2") @@ -688,7 +692,7 @@ def test_delete_mapping(self): ) mappings = listener.calls[0].mappings preset_name = listener.calls[0].name - expected_preset = Preset(get_preset_path("Foo Device", "preset2")) + expected_preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2")) expected_preset.load() expected_mappings = list(expected_preset) @@ -757,40 +761,40 @@ def test_cannot_set_autoload_without_preset(self): ) def test_finds_newest_group(self): - Preset(get_preset_path("Foo Device", "preset 1")).save() + Preset(PathUtils.get_preset_path("Foo Device", "preset 1")).save() time.sleep(0.01) - Preset(get_preset_path("Bar Device", "preset 2")).save() + Preset(PathUtils.get_preset_path("Bar Device", "preset 2")).save() self.assertEqual(self.data_manager.get_newest_group_key(), "Bar Device") def test_finds_newest_preset(self): - Preset(get_preset_path("Foo Device", "preset 1")).save() + Preset(PathUtils.get_preset_path("Foo Device", "preset 1")).save() time.sleep(0.01) - Preset(get_preset_path("Foo Device", "preset 2")).save() + Preset(PathUtils.get_preset_path("Foo Device", "preset 2")).save() self.data_manager.load_group("Foo Device") self.assertEqual(self.data_manager.get_newest_preset_name(), "preset 2") def test_newest_group_ignores_unknown_filetypes(self): - Preset(get_preset_path("Foo Device", "preset 1")).save() + Preset(PathUtils.get_preset_path("Foo Device", "preset 1")).save() time.sleep(0.01) - Preset(get_preset_path("Bar Device", "preset 2")).save() + Preset(PathUtils.get_preset_path("Bar Device", "preset 2")).save() # not a preset, ignore time.sleep(0.01) - path = os.path.join(get_preset_path("Foo Device"), "picture.png") + path = os.path.join(PathUtils.get_preset_path("Foo Device"), "picture.png") os.mknod(path) self.assertEqual(self.data_manager.get_newest_group_key(), "Bar Device") def test_newest_preset_ignores_unknown_filetypes(self): - Preset(get_preset_path("Bar Device", "preset 1")).save() + Preset(PathUtils.get_preset_path("Bar Device", "preset 1")).save() time.sleep(0.01) - Preset(get_preset_path("Bar Device", "preset 2")).save() + Preset(PathUtils.get_preset_path("Bar Device", "preset 2")).save() time.sleep(0.01) - Preset(get_preset_path("Bar Device", "preset 3")).save() + Preset(PathUtils.get_preset_path("Bar Device", "preset 3")).save() # not a preset, ignore time.sleep(0.01) - path = os.path.join(get_preset_path("Bar Device"), "picture.png") + path = os.path.join(PathUtils.get_preset_path("Bar Device"), "picture.png") os.mknod(path) self.data_manager.load_group("Bar Device") @@ -798,16 +802,18 @@ def test_newest_preset_ignores_unknown_filetypes(self): self.assertEqual(self.data_manager.get_newest_preset_name(), "preset 3") def test_newest_group_ignores_unknon_groups(self): - Preset(get_preset_path("Bar Device", "preset 1")).save() + Preset(PathUtils.get_preset_path("Bar Device", "preset 1")).save() time.sleep(0.01) - Preset(get_preset_path("unknown_group", "preset 2")).save() # not a known group + Preset( + PathUtils.get_preset_path("unknown_group", "preset 2") + ).save() # not a known group self.assertEqual(self.data_manager.get_newest_group_key(), "Bar Device") def test_newest_group_and_preset_raises_file_not_found(self): """should raise file not found error when all preset folders are empty""" self.assertRaises(FileNotFoundError, self.data_manager.get_newest_group_key) - os.makedirs(get_preset_path("Bar Device")) + os.makedirs(PathUtils.get_preset_path("Bar Device")) self.assertRaises(FileNotFoundError, self.data_manager.get_newest_group_key) self.data_manager.load_group("Bar Device") self.assertRaises(FileNotFoundError, self.data_manager.get_newest_preset_name) @@ -817,11 +823,11 @@ def test_newest_preset_raises_data_management_error(self): self.assertRaises(DataManagementError, self.data_manager.get_newest_preset_name) def test_newest_preset_only_searches_active_group(self): - Preset(get_preset_path("Foo Device", "preset 1")).save() + Preset(PathUtils.get_preset_path("Foo Device", "preset 1")).save() time.sleep(0.01) - Preset(get_preset_path("Foo Device", "preset 3")).save() + Preset(PathUtils.get_preset_path("Foo Device", "preset 3")).save() time.sleep(0.01) - Preset(get_preset_path("Bar Device", "preset 2")).save() + Preset(PathUtils.get_preset_path("Bar Device", "preset 2")).save() self.data_manager.load_group("Foo Device") self.assertEqual(self.data_manager.get_newest_preset_name(), "preset 3") @@ -833,7 +839,7 @@ def test_available_preset_name_default(self): ) def test_available_preset_name_adds_number_to_default(self): - Preset(get_preset_path("Foo Device", DEFAULT_PRESET_NAME)).save() + Preset(PathUtils.get_preset_path("Foo Device", DEFAULT_PRESET_NAME)).save() self.data_manager.load_group("Foo Device") self.assertEqual( self.data_manager.get_available_preset_name(), f"{DEFAULT_PRESET_NAME} 2" @@ -844,7 +850,7 @@ def test_available_preset_name_returns_provided_name(self): self.assertEqual(self.data_manager.get_available_preset_name("bar"), "bar") def test_available_preset_name__adds_number_to_provided_name(self): - Preset(get_preset_path("Foo Device", "bar")).save() + Preset(PathUtils.get_preset_path("Foo Device", "bar")).save() self.data_manager.load_group("Foo Device") self.assertEqual(self.data_manager.get_available_preset_name("bar"), "bar 2") @@ -860,27 +866,31 @@ def test_available_preset_name_sanitized(self): self.data_manager.get_available_preset_name(), DEFAULT_PRESET_NAME ) - Preset(get_preset_path("Qux/Device?", DEFAULT_PRESET_NAME)).save() + Preset(PathUtils.get_preset_path("Qux/Device?", DEFAULT_PRESET_NAME)).save() self.assertEqual( self.data_manager.get_available_preset_name(), f"{DEFAULT_PRESET_NAME} 2" ) - Preset(get_preset_path("Qux/Device?", "foo")).save() + Preset(PathUtils.get_preset_path("Qux/Device?", "foo")).save() self.assertEqual(self.data_manager.get_available_preset_name("foo"), "foo 2") def test_available_preset_name_increments_default(self): - Preset(get_preset_path("Foo Device", DEFAULT_PRESET_NAME)).save() - Preset(get_preset_path("Foo Device", f"{DEFAULT_PRESET_NAME} 2")).save() - Preset(get_preset_path("Foo Device", f"{DEFAULT_PRESET_NAME} 3")).save() + Preset(PathUtils.get_preset_path("Foo Device", DEFAULT_PRESET_NAME)).save() + Preset( + PathUtils.get_preset_path("Foo Device", f"{DEFAULT_PRESET_NAME} 2") + ).save() + Preset( + PathUtils.get_preset_path("Foo Device", f"{DEFAULT_PRESET_NAME} 3") + ).save() self.data_manager.load_group("Foo Device") self.assertEqual( self.data_manager.get_available_preset_name(), f"{DEFAULT_PRESET_NAME} 4" ) def test_available_preset_name_increments_provided_name(self): - Preset(get_preset_path("Foo Device", "foo")).save() - Preset(get_preset_path("Foo Device", "foo 1")).save() - Preset(get_preset_path("Foo Device", "foo 2")).save() + Preset(PathUtils.get_preset_path("Foo Device", "foo")).save() + Preset(PathUtils.get_preset_path("Foo Device", "foo 1")).save() + Preset(PathUtils.get_preset_path("Foo Device", "foo 2")).save() self.data_manager.load_group("Foo Device") self.assertEqual(self.data_manager.get_available_preset_name("foo 1"), "foo 3") diff --git a/tests/unit/test_event_pipeline/test_axis_transformation.py b/tests/unit/test_event_pipeline/test_axis_transformation.py index 6e19dbc01..1b5b8acc5 100644 --- a/tests/unit/test_event_pipeline/test_axis_transformation.py +++ b/tests/unit/test_event_pipeline/test_axis_transformation.py @@ -24,8 +24,10 @@ from typing import Iterable, List from inputremapper.injection.mapping_handlers.axis_transform import Transformation +from tests.new_test import setup_tests +@setup_tests class TestAxisTransformation(unittest.TestCase): @dataclasses.dataclass class InitArgs: diff --git a/tests/unit/test_event_pipeline/test_event_pipeline.py b/tests/unit/test_event_pipeline/test_event_pipeline.py index 078890cb1..e693b44d9 100644 --- a/tests/unit/test_event_pipeline/test_event_pipeline.py +++ b/tests/unit/test_event_pipeline/test_event_pipeline.py @@ -63,6 +63,7 @@ from tests.lib.logger import logger from tests.lib.constants import MAX_ABS, MIN_ABS from tests.lib.fixtures import Fixture, fixtures +from tests.new_test import setup_tests class EventPipelineTestBase(unittest.IsolatedAsyncioTestCase): @@ -108,6 +109,7 @@ def create_event_reader( return reader +@setup_tests class TestIdk(EventPipelineTestBase): async def test_any_event_as_button(self): """As long as there is an event handler and a mapping we should be able @@ -933,6 +935,7 @@ async def test_key_axis_combination_to_disable(self): ) +@setup_tests class TestAbsToAbs(EventPipelineTestBase): async def test_abs_to_abs(self): gain = 0.5 @@ -1033,6 +1036,7 @@ async def test_abs_to_abs_with_input_switch(self): ) +@setup_tests class TestRelToAbs(EventPipelineTestBase): async def test_rel_to_abs(self): timestamp = 0 @@ -1168,6 +1172,7 @@ async def test_rel_to_abs_with_input_switch(self): ) +@setup_tests class TestAbsToRel(EventPipelineTestBase): async def test_abs_to_rel(self): """Map gamepad EV_ABS events to EV_REL events.""" @@ -1314,6 +1319,7 @@ async def test_abs_to_wheel_hi_res_quirk(self): self.assertAlmostEqual(rel_hwheel, rel_hwheel_hi_res / 120, places=0) +@setup_tests class TestRelToBtn(EventPipelineTestBase): async def test_rel_to_btn(self): """Rel axis mapped to buttons are automatically released if no new rel event arrives.""" @@ -1446,6 +1452,7 @@ async def test_rel_trigger_threshold(self): ) +@setup_tests class TestAbsToBtn(EventPipelineTestBase): async def test_abs_trigger_threshold(self): """Test that different activation points for abs_to_btn work correctly.""" @@ -1514,6 +1521,7 @@ async def test_abs_trigger_threshold(self): self.assertEqual(len(forwarded_history), 0) +@setup_tests class TestRelToRel(EventPipelineTestBase): async def _test(self, input_code, input_value, output_code, output_value, gain=1): preset = Preset() diff --git a/tests/unit/test_event_pipeline/test_mapping_handlers.py b/tests/unit/test_event_pipeline/test_mapping_handlers.py index 05269889f..0bce2d7fe 100644 --- a/tests/unit/test_event_pipeline/test_mapping_handlers.py +++ b/tests/unit/test_event_pipeline/test_mapping_handlers.py @@ -64,10 +64,10 @@ from inputremapper.input_event import InputEvent, EventActions from tests.lib.cleanup import cleanup -from tests.lib.logger import logger from tests.lib.patches import InputDevice from tests.lib.constants import MAX_ABS from tests.lib.fixtures import fixtures +from tests.new_test import setup_tests class BaseTests: @@ -90,6 +90,7 @@ def test_reset(self): mock.reset.assert_called() +@setup_tests class TestAxisSwitchHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -110,6 +111,7 @@ def setUp(self): ) +@setup_tests class TestAbsToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -125,6 +127,7 @@ def setUp(self): ) +@setup_tests class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) @@ -151,6 +154,7 @@ async def test_reset(self): ) +@setup_tests class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)]) @@ -214,6 +218,7 @@ async def test_rate_stays(self): self.assertEqual(self.handler._observed_rate, DEFAULT_REL_RATE) +@setup_tests class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) @@ -242,6 +247,7 @@ async def test_reset(self): self.assertEqual(count, global_uinputs.get_uinput("mouse").write_count) +@setup_tests class TestCombinationHandler(BaseTests, unittest.IsolatedAsyncioTestCase): handler: CombinationHandler @@ -382,6 +388,7 @@ def test_no_forwards(self): self.assertListEqual(uinputs[self.keyboard_hash].write_history, []) +@setup_tests class TestHierarchyHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): self.mock1 = MagicMock() @@ -399,6 +406,7 @@ def test_reset(self): self.mock3.reset.assert_called() +@setup_tests class TestKeyHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -431,6 +439,7 @@ def test_reset(self): self.assertEqual(len(history), 2) +@setup_tests class TestMacroHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -470,6 +479,7 @@ async def test_reset(self): self.assertEqual(len(history), 4) +@setup_tests class TestRelToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -485,6 +495,7 @@ def setUp(self): ) +@setup_tests class TestRelToRelHanlder(BaseTests, unittest.IsolatedAsyncioTestCase): handler: RelToRelHandler diff --git a/tests/unit/test_event_reader.py b/tests/unit/test_event_reader.py index 650ec6b39..51e20621c 100644 --- a/tests/unit/test_event_reader.py +++ b/tests/unit/test_event_reader.py @@ -47,8 +47,10 @@ from inputremapper.utils import get_device_hash from tests.lib.fixtures import fixtures from tests.lib.cleanup import quick_cleanup +from tests.new_test import setup_tests +@setup_tests class TestEventReader(unittest.IsolatedAsyncioTestCase): def setUp(self): self.gamepad_source = evdev.InputDevice(fixtures.gamepad.path) diff --git a/tests/unit/test_global_uinputs.py b/tests/unit/test_global_uinputs.py index 199e483c5..61323609a 100644 --- a/tests/unit/test_global_uinputs.py +++ b/tests/unit/test_global_uinputs.py @@ -38,8 +38,10 @@ GlobalUInputs, ) from inputremapper.exceptions import EventNotHandled, UinputNotAvailable +from tests.new_test import setup_tests +@setup_tests class TestFrontendUinput(unittest.TestCase): def setUp(self) -> None: cleanup() @@ -57,6 +59,7 @@ def test_init(self): self.assertEqual(uinput_custom.capabilities(), capabilities) +@setup_tests class TestGlobalUinputs(unittest.TestCase): def setUp(self) -> None: cleanup() diff --git a/tests/unit/test_groups.py b/tests/unit/test_groups.py index 865ca85f3..b03a58c00 100644 --- a/tests/unit/test_groups.py +++ b/tests/unit/test_groups.py @@ -29,7 +29,7 @@ import evdev from evdev.ecodes import EV_KEY, KEY_A -from inputremapper.configs.paths import CONFIG_PATH +from inputremapper.configs.paths import PathUtils from inputremapper.groups import ( _FindGroups, groups, @@ -37,6 +37,7 @@ DeviceType, _Group, ) +from tests.new_test import setup_tests class FakePipe: @@ -46,6 +47,7 @@ def send(self, groups): self.groups = groups +@setup_tests class TestGroups(unittest.TestCase): def tearDown(self): quick_cleanup() @@ -61,7 +63,12 @@ def test_group(self): self.assertEqual(group.key, "key") self.assertEqual( group.get_preset_path("preset1234"), - os.path.join(CONFIG_PATH, "presets", group.name, "preset1234.json"), + os.path.join( + PathUtils.config_path(), + "presets", + group.name, + "preset1234.json", + ), ) def test_find_groups(self): diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index b04e2dce8..dfb3e6b52 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -71,6 +71,7 @@ from inputremapper.injection.macros.parse import parse from inputremapper.injection.context import Context from inputremapper.groups import groups, classify, DeviceType +from tests.new_test import setup_tests def wait_for_uinput_write(): @@ -80,6 +81,7 @@ def wait_for_uinput_write(): return float(time.time() - start) +@setup_tests class TestInjector(unittest.IsolatedAsyncioTestCase): new_gamepad_path = "/dev/input/event100" @@ -548,6 +550,7 @@ def test_is_in_capabilities(self): self.assertTrue(is_in_capabilities(key, capabilities)) +@setup_tests class TestModifyCapabilities(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/tests/unit/test_input_config.py b/tests/unit/test_input_config.py index 2d4c205ce..c8b096c81 100644 --- a/tests/unit/test_input_config.py +++ b/tests/unit/test_input_config.py @@ -44,8 +44,10 @@ ) from inputremapper.configs.input_config import InputCombination, InputConfig +from tests.new_test import setup_tests +@setup_tests class TestInputConfig(unittest.TestCase): def test_input_config(self): test_cases = [ @@ -320,6 +322,7 @@ def test_is_immutable(self): input_config.origin_hash = "foo" +@setup_tests class TestInputCombination(unittest.TestCase): def test_eq(self): a = InputCombination( diff --git a/tests/unit/test_input_event.py b/tests/unit/test_input_event.py index aab44bd86..81b28a43e 100644 --- a/tests/unit/test_input_event.py +++ b/tests/unit/test_input_event.py @@ -23,8 +23,10 @@ import evdev from dataclasses import FrozenInstanceError from inputremapper.input_event import InputEvent +from tests.new_test import setup_tests +@setup_tests class TestInputEvent(unittest.TestCase): def test_from_event(self): e1 = InputEvent.from_event(evdev.InputEvent(1, 2, 3, 4, 5)) diff --git a/tests/unit/test_ipc.py b/tests/unit/test_ipc.py index e055e9775..098a74b62 100644 --- a/tests/unit/test_ipc.py +++ b/tests/unit/test_ipc.py @@ -31,8 +31,10 @@ from inputremapper.ipc.pipe import Pipe from inputremapper.ipc.shared_dict import SharedDict from inputremapper.ipc.socket import Server, Client, Base +from tests.new_test import setup_tests +@setup_tests class TestSharedDict(unittest.TestCase): def setUp(self): self.shared_dict = SharedDict() @@ -52,6 +54,7 @@ def test_set_get(self): self.assertEqual(self.shared_dict["a"], 3) +@setup_tests class TestSocket(unittest.TestCase): def test_socket(self): def test(s1, s2): @@ -127,6 +130,7 @@ def test_base_abstract(self): self.assertRaises(NotImplementedError, lambda: Base.fileno(None)) +@setup_tests class TestPipe(unittest.IsolatedAsyncioTestCase): def test_pipe_single(self): p1 = Pipe(os.path.join(tmp, "pipe")) diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index f80454715..564fdcd8a 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -18,31 +18,37 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -import evdev +from tests.lib.tmp import tmp + +import logging import os import shutil import unittest -import logging -from tests.lib.tmp import tmp +import evdev -from inputremapper.logger import logger, update_verbosity, log_info, ColorfulFormatter -from inputremapper.configs.paths import remove +from inputremapper.configs.paths import PathUtils +from inputremapper.logger.logger import ( + logger, + ColorfulFormatter, +) +from tests.new_test import setup_tests -def add_filehandler(log_path): +def add_filehandler(log_path: str, debug: bool) -> None: """Start logging to a file.""" log_path = os.path.expanduser(log_path) os.makedirs(os.path.dirname(log_path), exist_ok=True) file_handler = logging.FileHandler(log_path) - file_handler.setFormatter(ColorfulFormatter()) + file_handler.setFormatter(ColorfulFormatter(debug)) logger.addHandler(file_handler) logger.info('Starting logging to "%s"', log_path) +@setup_tests class TestLogger(unittest.TestCase): def tearDown(self): - update_verbosity(debug=True) + logger.update_verbosity(debug=True) # remove the file handler logger.handlers = [ @@ -51,12 +57,12 @@ def tearDown(self): if not isinstance(logger.handlers, logging.FileHandler) ] path = os.path.join(tmp, "logger-test") - remove(path) + PathUtils.remove(path) def test_write(self): uinput = evdev.UInput(name="foo") path = os.path.join(tmp, "logger-test") - add_filehandler(path) + add_filehandler(path, False) logger.write((evdev.ecodes.EV_KEY, evdev.ecodes.KEY_A, 1), uinput) with open(path, "r") as f: content = f.read() @@ -66,10 +72,10 @@ def test_write(self): ) def test_log_info(self): - update_verbosity(debug=False) + logger.update_verbosity(debug=False) path = os.path.join(tmp, "logger-test") - add_filehandler(path) - log_info() + add_filehandler(path, False) + logger.log_info() with open(path, "r") as f: content = f.read().lower() self.assertIn("input-remapper", content) @@ -80,12 +86,13 @@ def test_makes_path(self): shutil.rmtree(path) new_path = os.path.join(tmp, "logger-test", "a", "b", "c") - add_filehandler(new_path) + add_filehandler(new_path, False) self.assertTrue(os.path.exists(new_path)) def test_debug(self): path = os.path.join(tmp, "logger-test") - add_filehandler(path) + logger.update_verbosity(True) + add_filehandler(path, True) logger.error("abc") logger.warning("foo") logger.info("123") @@ -112,8 +119,8 @@ def test_debug(self): def test_default(self): path = os.path.join(tmp, "logger-test") - update_verbosity(debug=False) - add_filehandler(path) + logger.update_verbosity(debug=False) + add_filehandler(path, False) logger.error("abc") logger.warning("foo") logger.info("123") diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index 939ac6526..1838f0510 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -71,6 +71,7 @@ from inputremapper.input_event import InputEvent from tests.lib.logger import logger from tests.lib.cleanup import quick_cleanup +from tests.new_test import setup_tests class MacroTestBase(unittest.IsolatedAsyncioTestCase): @@ -123,6 +124,7 @@ class DummyMapping: target_uinput = "keyboard + mouse" +@setup_tests class TestMacros(MacroTestBase): async def test_named_parameter(self): result = [] @@ -1152,6 +1154,7 @@ async def test_multiline_macro_and_comments(self): ) +@setup_tests class TestIfEq(MacroTestBase): async def test_ifeq_runs(self): # deprecated ifeq function, but kept for compatibility reasons @@ -1304,6 +1307,7 @@ def set_foo(value): ) +@setup_tests class TestIfSingle(MacroTestBase): async def test_if_single(self): macro = parse("if_single(key(x), key(y))", self.context, DummyMapping) @@ -1451,6 +1455,7 @@ async def test_if_single_ignores_joystick(self): self.assertListEqual(self.result, [(EV_KEY, code_a, 1), (EV_KEY, code_a, 0)]) +@setup_tests class TestIfTap(MacroTestBase): async def test_if_tap(self): macro = parse("if_tap(key(x), key(y), 100)", self.context, DummyMapping) diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py index 2c5bfb227..55e5f4496 100644 --- a/tests/unit/test_mapping.py +++ b/tests/unit/test_mapping.py @@ -40,8 +40,10 @@ from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.gui.messages.message_broker import MessageType +from tests.new_test import setup_tests +@setup_tests class TestMapping(unittest.IsolatedAsyncioTestCase): def test_init(self): """Test init and that defaults are set.""" @@ -410,6 +412,7 @@ def test_wrong_target_for_macro(self): ) +@setup_tests class TestUIMapping(unittest.IsolatedAsyncioTestCase): def test_init(self): """Should be able to initialize without throwing errors.""" diff --git a/tests/unit/test_message_broker.py b/tests/unit/test_message_broker.py index e5e45a4c1..d0ec410d2 100644 --- a/tests/unit/test_message_broker.py +++ b/tests/unit/test_message_broker.py @@ -22,6 +22,7 @@ from dataclasses import dataclass from inputremapper.gui.messages.message_broker import MessageBroker, MessageType, Signal +from tests.new_test import setup_tests class Listener: @@ -38,6 +39,7 @@ class Message: msg: str +@setup_tests class TestMessageBroker(unittest.TestCase): def test_calls_listeners(self): """The correct Listeners get called""" @@ -99,6 +101,7 @@ def listener4(_): self.assertEqual([4, 4, 4], calls[3:]) +@setup_tests class TestSignal(unittest.TestCase): def test_eq(self): self.assertEqual(Signal(MessageType.uinputs), Signal(MessageType.uinputs)) diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index fa56abbb4..5db3157b6 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -12,16 +12,15 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . - from tests.lib.cleanup import quick_cleanup from tests.lib.tmp import tmp +import json import os -import unittest import shutil -import json -import pkg_resources +import unittest +import pkg_resources from evdev.ecodes import ( EV_KEY, EV_ABS, @@ -35,40 +34,33 @@ REL_Y, REL_WHEEL_HI_RES, REL_HWHEEL_HI_RES, - KEY_A, ) -from inputremapper.configs.mapping import UIMapping -from inputremapper.configs.migrations import migrate, config_version -from inputremapper.configs.preset import Preset from inputremapper.configs.global_config import global_config -from inputremapper.configs.paths import ( - touch, - CONFIG_PATH, - mkdir, - get_preset_path, - get_config_path, - remove, -) from inputremapper.configs.input_config import InputCombination, InputConfig -from inputremapper.user import HOME - -from inputremapper.logger import VERSION +from inputremapper.configs.mapping import UIMapping +from inputremapper.configs.migrations import Migrations +from inputremapper.configs.paths import PathUtils +from inputremapper.configs.preset import Preset +from inputremapper.logger.logger import VERSION +from inputremapper.user import UserUtils +from tests.new_test import setup_tests +@setup_tests class TestMigrations(unittest.TestCase): def setUp(self): # some extra care to ensure those tests are not destroying actual presets - self.assertTrue(HOME.startswith("/tmp")) - self.assertTrue(CONFIG_PATH.startswith("/tmp")) - self.assertTrue(get_preset_path().startswith("/tmp")) - self.assertTrue(get_preset_path("foo", "bar").startswith("/tmp")) - self.assertTrue(get_config_path().startswith("/tmp")) - self.assertTrue(get_config_path("foo").startswith("/tmp")) - - self.v1_dir = os.path.join(HOME, ".config", "input-remapper") + self.assertTrue(UserUtils.home.startswith("/tmp")) + self.assertTrue(PathUtils.config_path().startswith("/tmp")) + self.assertTrue(PathUtils.get_preset_path().startswith("/tmp")) + self.assertTrue(PathUtils.get_preset_path("foo", "bar").startswith("/tmp")) + self.assertTrue(PathUtils.get_config_path().startswith("/tmp")) + self.assertTrue(PathUtils.get_config_path("foo").startswith("/tmp")) + + self.v1_dir = os.path.join(UserUtils.home, ".config", "input-remapper") self.beta_dir = os.path.join( - HOME, ".config", "input-remapper", "beta_1.6.0-beta" + UserUtils.home, ".config", "input-remapper", "beta_1.6.0-beta" ) def tearDown(self): @@ -76,25 +68,25 @@ def tearDown(self): self.assertEqual(len(global_config.iterate_autoload_presets()), 0) def test_migrate_suffix(self): - old = os.path.join(CONFIG_PATH, "config") - new = os.path.join(CONFIG_PATH, "config.json") + old = os.path.join(PathUtils.config_path(), "config") + new = os.path.join(PathUtils.config_path(), "config.json") try: os.remove(new) except FileNotFoundError: pass - touch(old) + PathUtils.touch(old) with open(old, "w") as f: f.write("{}") - migrate() + Migrations.migrate() self.assertTrue(os.path.exists(new)) self.assertFalse(os.path.exists(old)) def test_rename_config(self): - old = os.path.join(HOME, ".config", "key-mapper") - new = CONFIG_PATH + old = os.path.join(UserUtils.home, ".config", "key-mapper") + new = PathUtils.config_path() # we are not destroying our actual config files with this test self.assertTrue(new.startswith(tmp), f'Expected "{new}" to start with "{tmp}"') @@ -105,11 +97,11 @@ def test_rename_config(self): pass old_config_json = os.path.join(old, "config.json") - touch(old_config_json) + PathUtils.touch(old_config_json) with open(old_config_json, "w") as f: f.write('{"foo":"bar"}') - migrate() + Migrations.migrate() self.assertTrue(os.path.exists(new)) self.assertFalse(os.path.exists(old)) @@ -120,29 +112,29 @@ def test_rename_config(self): self.assertEqual(moved_config["foo"], "bar") def test_wont_migrate_suffix(self): - old = os.path.join(CONFIG_PATH, "config") - new = os.path.join(CONFIG_PATH, "config.json") + old = os.path.join(PathUtils.config_path(), "config") + new = os.path.join(PathUtils.config_path(), "config.json") - touch(new) + PathUtils.touch(new) with open(new, "w") as f: f.write("{}") - touch(old) + PathUtils.touch(old) with open(old, "w") as f: f.write("{}") - migrate() + Migrations.migrate() self.assertTrue(os.path.exists(new)) self.assertTrue(os.path.exists(old)) def test_migrate_preset(self): - if os.path.exists(CONFIG_PATH): - shutil.rmtree(CONFIG_PATH) + if os.path.exists(PathUtils.config_path()): + shutil.rmtree(PathUtils.config_path()) - p1 = os.path.join(CONFIG_PATH, "foo1", "bar1.json") - p2 = os.path.join(CONFIG_PATH, "foo2", "bar2.json") - touch(p1) - touch(p2) + p1 = os.path.join(PathUtils.config_path(), "foo1", "bar1.json") + p2 = os.path.join(PathUtils.config_path(), "foo2", "bar2.json") + PathUtils.touch(p1) + PathUtils.touch(p2) with open(p1, "w") as f: f.write("{}") @@ -150,26 +142,34 @@ def test_migrate_preset(self): with open(p2, "w") as f: f.write("{}") - migrate() + Migrations.migrate() - self.assertFalse(os.path.exists(os.path.join(CONFIG_PATH, "foo1", "bar1.json"))) - self.assertFalse(os.path.exists(os.path.join(CONFIG_PATH, "foo2", "bar2.json"))) + self.assertFalse( + os.path.exists(os.path.join(PathUtils.config_path(), "foo1", "bar1.json")) + ) + self.assertFalse( + os.path.exists(os.path.join(PathUtils.config_path(), "foo2", "bar2.json")) + ) self.assertTrue( - os.path.exists(os.path.join(CONFIG_PATH, "presets", "foo1", "bar1.json")), + os.path.exists( + os.path.join(PathUtils.config_path(), "presets", "foo1", "bar1.json") + ), ) self.assertTrue( - os.path.exists(os.path.join(CONFIG_PATH, "presets", "foo2", "bar2.json")), + os.path.exists( + os.path.join(PathUtils.config_path(), "presets", "foo2", "bar2.json") + ), ) def test_wont_migrate_preset(self): - if os.path.exists(CONFIG_PATH): - shutil.rmtree(CONFIG_PATH) + if os.path.exists(PathUtils.config_path()): + shutil.rmtree(PathUtils.config_path()) - p1 = os.path.join(CONFIG_PATH, "foo1", "bar1.json") - p2 = os.path.join(CONFIG_PATH, "foo2", "bar2.json") - touch(p1) - touch(p2) + p1 = os.path.join(PathUtils.config_path(), "foo1", "bar1.json") + p2 = os.path.join(PathUtils.config_path(), "foo2", "bar2.json") + PathUtils.touch(p1) + PathUtils.touch(p2) with open(p1, "w") as f: f.write("{}") @@ -178,18 +178,26 @@ def test_wont_migrate_preset(self): f.write("{}") # already migrated - mkdir(os.path.join(CONFIG_PATH, "presets")) + PathUtils.mkdir(os.path.join(PathUtils.config_path(), "presets")) - migrate() + Migrations.migrate() - self.assertTrue(os.path.exists(os.path.join(CONFIG_PATH, "foo1", "bar1.json"))) - self.assertTrue(os.path.exists(os.path.join(CONFIG_PATH, "foo2", "bar2.json"))) + self.assertTrue( + os.path.exists(os.path.join(PathUtils.config_path(), "foo1", "bar1.json")) + ) + self.assertTrue( + os.path.exists(os.path.join(PathUtils.config_path(), "foo2", "bar2.json")) + ) self.assertFalse( - os.path.exists(os.path.join(CONFIG_PATH, "presets", "foo1", "bar1.json")), + os.path.exists( + os.path.join(PathUtils.config_path(), "presets", "foo1", "bar1.json") + ), ) self.assertFalse( - os.path.exists(os.path.join(CONFIG_PATH, "presets", "foo2", "bar2.json")), + os.path.exists( + os.path.join(PathUtils.config_path(), "presets", "foo2", "bar2.json") + ), ) def test_migrate_mappings(self): @@ -199,7 +207,9 @@ def test_migrate_mappings(self): {(type, code): symbol} or {(type, code, value): symbol} should migrate to {InputCombination: {target: target, symbol: symbol, ...}} """ - path = os.path.join(CONFIG_PATH, "presets", "Foo Device", "test.json") + path = os.path.join( + PathUtils.config_path(), "presets", "Foo Device", "test.json" + ) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as file: json.dump( @@ -222,9 +232,9 @@ def test_migrate_mappings(self): }, file, ) - migrate() + Migrations.migrate() # use UIMapping to also load invalid mappings - preset = Preset(get_preset_path("Foo Device", "test"), UIMapping) + preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() self.assertEqual( @@ -311,7 +321,9 @@ def test_migrate_mappings(self): self.assertEqual(8, len(preset)) def test_migrate_otherwise(self): - path = os.path.join(CONFIG_PATH, "presets", "Foo Device", "test.json") + path = os.path.join( + PathUtils.config_path(), "presets", "Foo Device", "test.json" + ) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as file: json.dump( @@ -327,9 +339,9 @@ def test_migrate_otherwise(self): file, ) - migrate() + Migrations.migrate() - preset = Preset(get_preset_path("Foo Device", "test"), UIMapping) + preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() self.assertEqual( @@ -374,39 +386,45 @@ def test_migrate_otherwise(self): ) def test_add_version(self): - path = os.path.join(CONFIG_PATH, "config.json") + path = os.path.join(PathUtils.config_path(), "config.json") os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as file: file.write("{}") - migrate() - self.assertEqual(pkg_resources.parse_version(VERSION), config_version()) + Migrations.migrate() + self.assertEqual( + pkg_resources.parse_version(VERSION), Migrations.config_version() + ) def test_update_version(self): - path = os.path.join(CONFIG_PATH, "config.json") + path = os.path.join(PathUtils.config_path(), "config.json") os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as file: json.dump({"version": "0.1.0"}, file) - migrate() - self.assertEqual(pkg_resources.parse_version(VERSION), config_version()) + Migrations.migrate() + self.assertEqual( + pkg_resources.parse_version(VERSION), Migrations.config_version() + ) def test_config_version(self): - path = os.path.join(CONFIG_PATH, "config.json") + path = os.path.join(PathUtils.config_path(), "config.json") with open(path, "w") as file: file.write("{}") - self.assertEqual("0.0.0", config_version().public) + self.assertEqual("0.0.0", Migrations.config_version().public) try: os.remove(path) except FileNotFoundError: pass - self.assertEqual("0.0.0", config_version().public) + self.assertEqual("0.0.0", Migrations.config_version().public) def test_migrate_left_and_right_purpose(self): - path = os.path.join(CONFIG_PATH, "presets", "Foo Device", "test.json") + path = os.path.join( + PathUtils.config_path(), "presets", "Foo Device", "test.json" + ) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as file: json.dump( @@ -423,9 +441,9 @@ def test_migrate_left_and_right_purpose(self): }, file, ) - migrate() + Migrations.migrate() - preset = Preset(get_preset_path("Foo Device", "test"), UIMapping) + preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() # 2 mappings for mouse # 2 mappings for wheel @@ -490,7 +508,9 @@ def test_migrate_left_and_right_purpose(self): def test_migrate_left_and_right_purpose2(self): # same as above, but left and right is swapped - path = os.path.join(CONFIG_PATH, "presets", "Foo Device", "test.json") + path = os.path.join( + PathUtils.config_path(), "presets", "Foo Device", "test.json" + ) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as file: json.dump( @@ -507,9 +527,9 @@ def test_migrate_left_and_right_purpose2(self): }, file, ) - migrate() + Migrations.migrate() - preset = Preset(get_preset_path("Foo Device", "test"), UIMapping) + preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() # 2 mappings for mouse # 2 mappings for wheel @@ -575,7 +595,7 @@ def _create_v1_setup(self): """Create all files needed to mimic an outdated v1 configuration.""" device_name = "device_name" - mkdir(os.path.join(self.v1_dir, "presets", device_name)) + PathUtils.mkdir(os.path.join(self.v1_dir, "presets", device_name)) v1_config = {"autoload": {device_name: "foo"}, "version": "1.0"} with open(os.path.join(self.v1_dir, "config.json"), "w") as file: json.dump(v1_config, file) @@ -591,7 +611,7 @@ def _create_beta_setup(self): device_name = "device_name" # same here, but a different contents to tell the difference - mkdir(os.path.join(self.beta_dir, "presets", device_name)) + PathUtils.mkdir(os.path.join(self.beta_dir, "presets", device_name)) beta_config = {"autoload": {device_name: "bar"}, "version": "1.6"} with open(os.path.join(self.beta_dir, "config.json"), "w") as file: json.dump(beta_config, file) @@ -614,20 +634,20 @@ def _create_beta_setup(self): def test_prioritize_v1_over_beta_configs(self): # if both v1 and beta presets and config exist, migrate v1 - remove(get_config_path()) + PathUtils.remove(PathUtils.get_config_path()) device_name = "device_name" self._create_v1_setup() self._create_beta_setup() - self.assertFalse(os.path.exists(get_preset_path(device_name, "foo"))) - self.assertFalse(os.path.exists(get_config_path("config.json"))) + self.assertFalse(os.path.exists(PathUtils.get_preset_path(device_name, "foo"))) + self.assertFalse(os.path.exists(PathUtils.get_config_path("config.json"))) - migrate() + Migrations.migrate() - self.assertTrue(os.path.exists(get_preset_path(device_name, "foo"))) - self.assertTrue(os.path.exists(get_config_path("config.json"))) - self.assertFalse(os.path.exists(get_preset_path(device_name, "bar"))) + self.assertTrue(os.path.exists(PathUtils.get_preset_path(device_name, "foo"))) + self.assertTrue(os.path.exists(PathUtils.get_config_path("config.json"))) + self.assertFalse(os.path.exists(PathUtils.get_preset_path(device_name, "bar"))) # expect all original files to still exist self.assertTrue(os.path.join(self.v1_dir, "config.json")) @@ -636,13 +656,13 @@ def test_prioritize_v1_over_beta_configs(self): self.assertTrue(os.path.join(self.beta_dir, "presets", "bar.json")) # v1 configs should be in the v2 dir now, and migrated - with open(get_config_path("config.json"), "r") as f: + with open(PathUtils.get_config_path("config.json"), "r") as f: config_json = json.load(f) self.assertDictEqual( config_json, {"autoload": {device_name: "foo"}, "version": VERSION} ) - with open(get_preset_path(device_name, "foo.json"), "r") as f: - os.system(f'cat { get_preset_path(device_name, "foo.json") }') + with open(PathUtils.get_preset_path(device_name, "foo.json"), "r") as f: + os.system(f'cat { PathUtils.get_preset_path(device_name, "foo.json") }') preset_foo_json = json.load(f) self.assertEqual( preset_foo_json, @@ -661,31 +681,31 @@ def test_prioritize_v1_over_beta_configs(self): def test_copy_over_beta_configs(self): # same as test_prioritize_v1_over_beta_configs, but only create the beta # directory without any v1 presets. - remove(get_config_path()) + PathUtils.remove(PathUtils.get_config_path()) device_name = "device_name" self._create_beta_setup() - self.assertFalse(os.path.exists(get_preset_path(device_name, "bar"))) - self.assertFalse(os.path.exists(get_config_path("config.json"))) + self.assertFalse(os.path.exists(PathUtils.get_preset_path(device_name, "bar"))) + self.assertFalse(os.path.exists(PathUtils.get_config_path("config.json"))) - migrate() + Migrations.migrate() - self.assertTrue(os.path.exists(get_preset_path(device_name, "bar"))) - self.assertTrue(os.path.exists(get_config_path("config.json"))) + self.assertTrue(os.path.exists(PathUtils.get_preset_path(device_name, "bar"))) + self.assertTrue(os.path.exists(PathUtils.get_config_path("config.json"))) # expect all original files to still exist self.assertTrue(os.path.join(self.beta_dir, "config.json")) self.assertTrue(os.path.join(self.beta_dir, "presets", "bar.json")) # beta configs should be in the v2 dir now - with open(get_config_path("config.json"), "r") as f: + with open(PathUtils.get_config_path("config.json"), "r") as f: config_json = json.load(f) self.assertDictEqual( config_json, {"autoload": {device_name: "bar"}, "version": VERSION} ) - with open(get_preset_path(device_name, "bar.json"), "r") as f: - os.system(f'cat { get_preset_path(device_name, "bar.json") }') + with open(PathUtils.get_preset_path(device_name, "bar.json"), "r") as f: + os.system(f'cat { PathUtils.get_preset_path(device_name, "bar.json") }') preset_foo_json = json.load(f) self.assertEqual( preset_foo_json, diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py index 6be62be0d..b54caf0c5 100644 --- a/tests/unit/test_paths.py +++ b/tests/unit/test_paths.py @@ -26,19 +26,15 @@ import unittest import tempfile -from inputremapper.configs.paths import ( - touch, - mkdir, - get_preset_path, - get_config_path, - split_all, -) +from inputremapper.configs.paths import PathUtils +from tests.new_test import setup_tests def _raise(error): raise error +@setup_tests class TestPaths(unittest.TestCase): def tearDown(self): quick_cleanup() @@ -46,31 +42,37 @@ def tearDown(self): def test_touch(self): with tempfile.TemporaryDirectory() as local_tmp: path_abcde = os.path.join(local_tmp, "a/b/c/d/e") - touch(path_abcde) + PathUtils.touch(path_abcde) self.assertTrue(os.path.exists(path_abcde)) self.assertTrue(os.path.isfile(path_abcde)) self.assertRaises( ValueError, - lambda: touch(os.path.join(local_tmp, "a/b/c/d/f/")), + lambda: PathUtils.touch(os.path.join(local_tmp, "a/b/c/d/f/")), ) def test_mkdir(self): with tempfile.TemporaryDirectory() as local_tmp: path_bcde = os.path.join(local_tmp, "b/c/d/e") - mkdir(path_bcde) + PathUtils.mkdir(path_bcde) self.assertTrue(os.path.exists(path_bcde)) self.assertTrue(os.path.isdir(path_bcde)) def test_get_preset_path(self): - self.assertTrue(get_preset_path().startswith(get_config_path())) - self.assertTrue(get_preset_path().endswith("presets")) - self.assertTrue(get_preset_path("a").endswith("presets/a")) - self.assertTrue(get_preset_path("a", "b").endswith("presets/a/b.json")) + self.assertTrue( + PathUtils.get_preset_path().startswith(PathUtils.get_config_path()) + ) + self.assertTrue(PathUtils.get_preset_path().endswith("presets")) + self.assertTrue(PathUtils.get_preset_path("a").endswith("presets/a")) + self.assertTrue( + PathUtils.get_preset_path("a", "b").endswith("presets/a/b.json") + ) def test_get_config_path(self): # might end with /beta_XXX - self.assertTrue(get_config_path().startswith(f"{tmp}/.config/input-remapper")) - self.assertTrue(get_config_path("a", "b").endswith("a/b")) + self.assertTrue( + PathUtils.get_config_path().startswith(f"{tmp}/.config/input-remapper") + ) + self.assertTrue(PathUtils.get_config_path("a", "b").endswith("a/b")) def test_split_all(self): - self.assertListEqual(split_all("a/b/c/d"), ["a", "b", "c", "d"]) + self.assertListEqual(PathUtils.split_all("a/b/c/d"), ["a", "b", "c", "d"]) diff --git a/tests/unit/test_preset.py b/tests/unit/test_preset.py index 7ee781050..d03d34066 100644 --- a/tests/unit/test_preset.py +++ b/tests/unit/test_preset.py @@ -26,15 +26,17 @@ from inputremapper.configs.mapping import Mapping from inputremapper.configs.mapping import UIMapping -from inputremapper.configs.paths import get_preset_path, get_config_path, CONFIG_PATH +from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.configs.input_config import InputCombination, InputConfig from tests.lib.cleanup import quick_cleanup +from tests.new_test import setup_tests +@setup_tests class TestPreset(unittest.TestCase): def setUp(self): - self.preset = Preset(get_preset_path("foo", "bar2")) + self.preset = Preset(PathUtils.get_preset_path("foo", "bar2")) self.assertFalse(self.preset.has_unsaved_changes()) def tearDown(self): @@ -62,7 +64,7 @@ def test_is_mapped_multiple_times(self): self.assertTrue(self.preset._is_mapped_multiple_times(permutations[2])) def test_has_unsaved_changes(self): - self.preset.path = get_preset_path("foo", "bar2") + self.preset.path = PathUtils.get_preset_path("foo", "bar2") self.preset.add(Mapping.from_combination()) self.assertTrue(self.preset.has_unsaved_changes()) self.preset.save() @@ -82,12 +84,12 @@ def test_has_unsaved_changes(self): self.assertFalse(self.preset.has_unsaved_changes()) # change the path to a non exiting file - self.preset.path = get_preset_path("bar", "foo") + self.preset.path = PathUtils.get_preset_path("bar", "foo") # the preset has a mapping, the file has not self.assertTrue(self.preset.has_unsaved_changes()) # change back to the original path - self.preset.path = get_preset_path("foo", "bar2") + self.preset.path = PathUtils.get_preset_path("foo", "bar2") # no difference between file and memory self.assertFalse(self.preset.has_unsaved_changes()) @@ -97,12 +99,12 @@ def test_has_unsaved_changes(self): self.assertTrue(self.preset.has_unsaved_changes()) self.preset.load() - self.preset.path = get_preset_path("bar", "foo") + self.preset.path = PathUtils.get_preset_path("bar", "foo") self.preset.remove(Mapping.from_combination().input_combination) # empty preset and empty file self.assertFalse(self.preset.has_unsaved_changes()) - self.preset.path = get_preset_path("foo", "bar2") + self.preset.path = PathUtils.get_preset_path("foo", "bar2") # empty preset, but non-empty file self.assertTrue(self.preset.has_unsaved_changes()) self.preset.load() @@ -130,13 +132,15 @@ def test_save_load(self): self.preset.add( Mapping.from_combination(InputCombination((two, three)), "keyboard", "3"), ) - self.preset.path = get_preset_path("Foo Device", "test") + self.preset.path = PathUtils.get_preset_path("Foo Device", "test") self.preset.save() - path = os.path.join(CONFIG_PATH, "presets", "Foo Device", "test.json") + path = os.path.join( + PathUtils.config_path(), "presets", "Foo Device", "test.json" + ) self.assertTrue(os.path.exists(path)) - loaded = Preset(get_preset_path("Foo Device", "test")) + loaded = Preset(PathUtils.get_preset_path("Foo Device", "test")) self.assertEqual(len(loaded), 0) loaded.load() @@ -156,7 +160,7 @@ def test_save_load(self): ) # load missing file - preset = Preset(get_config_path("missing_file.json")) + preset = Preset(PathUtils.get_config_path("missing_file.json")) self.assertRaises(FileNotFoundError, preset.load) def test_modify_mapping(self): @@ -219,11 +223,11 @@ def test_modify_mapping(self): def test_avoids_redundant_saves(self): with patch.object(self.preset, "has_unsaved_changes", lambda: False): - self.preset.path = get_preset_path("foo", "bar2") + self.preset.path = PathUtils.get_preset_path("foo", "bar2") self.preset.add(Mapping.from_combination()) self.preset.save() - with open(get_preset_path("foo", "bar2"), "r") as f: + with open(PathUtils.get_preset_path("foo", "bar2"), "r") as f: content = f.read() self.assertFalse(content) @@ -350,12 +354,12 @@ def test_empty(self): ), ) self.assertEqual(len(self.preset), 3) - self.preset.path = get_config_path("test.json") + self.preset.path = PathUtils.get_config_path("test.json") self.preset.save() self.assertFalse(self.preset.has_unsaved_changes()) self.preset.empty() - self.assertEqual(self.preset.path, get_config_path("test.json")) + self.assertEqual(self.preset.path, PathUtils.get_config_path("test.json")) self.assertTrue(self.preset.has_unsaved_changes()) self.assertEqual(len(self.preset), 0) @@ -382,7 +386,7 @@ def test_clear(self): ), ) self.assertEqual(len(self.preset), 3) - self.preset.path = get_config_path("test.json") + self.preset.path = PathUtils.get_config_path("test.json") self.preset.save() self.assertFalse(self.preset.has_unsaved_changes()) @@ -435,7 +439,9 @@ def test_dangerously_mapped_btn_left(self): self.assertFalse(self.preset.dangerously_mapped_btn_left()) def test_save_load_with_invalid_mappings(self): - ui_preset = Preset(get_config_path("test.json"), mapping_factory=UIMapping) + ui_preset = Preset( + PathUtils.get_config_path("test.json"), mapping_factory=UIMapping + ) ui_preset.add(UIMapping()) self.assertFalse(ui_preset.is_valid()) @@ -454,7 +460,7 @@ def test_save_load_with_invalid_mappings(self): ui_preset.save() # only the valid preset is loaded - preset = Preset(get_config_path("test.json")) + preset = Preset(PathUtils.get_config_path("test.json")) preset.load() self.assertEqual(len(preset), 1) @@ -467,7 +473,7 @@ def test_save_load_with_invalid_mappings(self): # both presets load ui_preset.clear() - ui_preset.path = get_config_path("test.json") + ui_preset.path = PathUtils.get_config_path("test.json") ui_preset.load() self.assertEqual(len(ui_preset), 2) diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index fd486dedc..fdf433c3d 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -64,6 +64,7 @@ from tests.lib.pipes import push_event, push_events from tests.lib.fixtures import fixtures from tests.lib.stuff import spy +from tests.new_test import setup_tests CODE_1 = 100 CODE_2 = 101 @@ -89,6 +90,7 @@ def wait(func, timeout=1.0): break +@setup_tests class TestReaderAsyncio(unittest.IsolatedAsyncioTestCase): def setUp(self): self.reader_service = None @@ -157,6 +159,7 @@ def remember_context(*args, **kwargs): self.assertEqual([call[0] for call in write_spy.call_args_list], events) +@setup_tests class TestReaderMultiprocessing(unittest.TestCase): def setUp(self): self.reader_service_process = None diff --git a/tests/unit/test_system_mapping.py b/tests/unit/test_system_mapping.py index ab4010dbf..8121c730c 100644 --- a/tests/unit/test_system_mapping.py +++ b/tests/unit/test_system_mapping.py @@ -26,11 +26,13 @@ from evdev.ecodes import BTN_LEFT, KEY_A -from inputremapper.configs.paths import CONFIG_PATH +from inputremapper.configs.paths import PathUtils from inputremapper.configs.system_mapping import SystemMapping, XMODMAP_FILENAME from tests.lib.cleanup import quick_cleanup +from tests.new_test import setup_tests +@setup_tests class TestSystemMapping(unittest.TestCase): def tearDown(self): quick_cleanup() @@ -44,7 +46,7 @@ def test_update(self): def test_xmodmap_file(self): system_mapping = SystemMapping() - path = os.path.join(CONFIG_PATH, XMODMAP_FILENAME) + path = os.path.join(PathUtils.config_path(), XMODMAP_FILENAME) os.remove(path) system_mapping.populate() @@ -70,7 +72,7 @@ def check_output(*args, **kwargs): with patch.object(subprocess, "check_output", check_output): system_mapping = SystemMapping() - path = os.path.join(CONFIG_PATH, XMODMAP_FILENAME) + path = os.path.join(PathUtils.config_path(), XMODMAP_FILENAME) os.remove(path) system_mapping.populate() @@ -83,7 +85,7 @@ def check_output(*args, **kwargs): with patch.object(subprocess, "check_output", check_output): system_mapping = SystemMapping() - path = os.path.join(CONFIG_PATH, XMODMAP_FILENAME) + path = os.path.join(PathUtils.config_path(), XMODMAP_FILENAME) os.remove(path) system_mapping.populate() diff --git a/tests/unit/test_test.py b/tests/unit/test_test.py index 5c53bd90c..e11169465 100644 --- a/tests/unit/test_test.py +++ b/tests/unit/test_test.py @@ -40,8 +40,10 @@ from inputremapper.input_event import InputEvent from inputremapper.utils import get_device_hash from inputremapper.gui.messages.message_broker import MessageBroker +from tests.new_test import setup_tests +@setup_tests class TestTest(unittest.TestCase): def test_stubs(self): self.assertIsNotNone(groups.find(key="Foo Device 2")) diff --git a/tests/unit/test_user.py b/tests/unit/test_user.py index 1322dbe97..c9bae70b7 100644 --- a/tests/unit/test_user.py +++ b/tests/unit/test_user.py @@ -25,23 +25,25 @@ import unittest from unittest import mock -from inputremapper.user import get_user, get_home +from inputremapper.user import UserUtils +from tests.new_test import setup_tests def _raise(error): raise error +@setup_tests class TestUser(unittest.TestCase): def tearDown(self): quick_cleanup() def test_get_user(self): with mock.patch("os.getlogin", lambda: "foo"): - self.assertEqual(get_user(), "foo") + self.assertEqual(UserUtils.get_user(), "foo") with mock.patch("os.getlogin", lambda: "root"): - self.assertEqual(get_user(), "root") + self.assertEqual(UserUtils.get_user(), "root") property_mock = mock.Mock() property_mock.configure_mock(pw_name="quix") @@ -50,15 +52,15 @@ def test_get_user(self): ): os.environ["USER"] = "root" os.environ["SUDO_USER"] = "qux" - self.assertEqual(get_user(), "qux") + self.assertEqual(UserUtils.get_user(), "qux") os.environ["USER"] = "root" del os.environ["SUDO_USER"] os.environ["PKEXEC_UID"] = "1000" - self.assertNotEqual(get_user(), "root") + self.assertNotEqual(UserUtils.get_user(), "root") def test_get_home(self): property_mock = mock.Mock() property_mock.configure_mock(pw_dir="/custom/home/foo") with mock.patch("pwd.getpwnam", return_value=property_mock): - self.assertEqual(get_home("foo"), "/custom/home/foo") + self.assertEqual(UserUtils.get_home("foo"), "/custom/home/foo") diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index ef0e28d9a..71648a5ed 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -24,8 +24,10 @@ from evdev._ecodes import EV_ABS, ABS_X, BTN_WEST, BTN_Y, EV_KEY, KEY_A from inputremapper.utils import get_evdev_constant_name +from tests.new_test import setup_tests +@setup_tests class TestUtil(unittest.TestCase): def test_get_evdev_constant_name(self): # BTN_WEST and BTN_Y both are code 308. I don't care which one is chosen From f705d5fd024d75c34af403ab5d2baebb69df5e4c Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sun, 29 Sep 2024 10:56:17 +0200 Subject: [PATCH 02/32] asdf --- tests/new_test.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/new_test.py b/tests/new_test.py index 9b898182f..73f4a5a5d 100644 --- a/tests/new_test.py +++ b/tests/new_test.py @@ -124,7 +124,3 @@ def tearDown(self): cls.tearDownClass = tearDownClass return cls - - -# TODO maybe make a PR for just the test-setup, without PathUtils and UserUtils. -# Then prepare another PR for DI. From 12a2d721fce2dc224d879f1e0831570547d2c307 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Sun, 29 Sep 2024 21:33:03 +0200 Subject: [PATCH 03/32] update usage for combinations --- readme/usage.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/readme/usage.md b/readme/usage.md index 422bb78bf..707aff3a6 100644 --- a/readme/usage.md +++ b/readme/usage.md @@ -49,14 +49,20 @@ on the `Advanced` button. A mapping with an input combination is only injected once all combination keys are pressed. This means all the input keys you press before the combination is complete -will be injected unmodified. In some cases this can be desirable, in others not. -In the advanced input configuration there is the `Release Input` toggle. +will be injected unmodified. In some cases this can be desirable, in others not. + +*Option 1*: In the advanced input configuration there is the `Release Input` toggle. This will release all inputs which are part of the combination before the mapping is injected. Consider a mapping `Shift+1 -> a` this will inject a lowercase `a` if the toggle is on and an uppercase `A` if it is off. The exact behaviour if the toggle is off is dependent on keys (are modifiers involved?), the order in which they are pressed and on your environment (X11/Wayland). By default the toggle is on. +*Option 2*: Disable the keys that are part of the combination individually. So with +a mapping of `Super+1 -> a`, you could additionally map `Super` to `disable`. Now +`Super` won't do anything anymore, and therefore pressing the combination won't have +any side effects anymore. + ## Writing Combinations You can write `Control_L + a` as mapping, which will inject those two From 88a13a901590eb45a780a624eb36863ee0fd05cc Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:57:06 +0200 Subject: [PATCH 04/32] development.md hint for evedv imports and patches --- readme/development.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/readme/development.md b/readme/development.md index 6f32c0b74..88ea87177 100644 --- a/readme/development.md +++ b/readme/development.md @@ -90,6 +90,13 @@ completely. Use `get_children()` and iterate over it with regular python `for` l Use `gtk_iteration()` in tests when interacting with GTK methods to trigger events to be emitted. +Do not do `from evdev import list_devices; list_devices()`, and instead do +`import evdev; evdev.list_devices()`. The first variant cannot be easily patched in +tests (there are ways, but as far as I can tell it has to be configured individually +for each source-file/module). The second option allows for patches to be defiend in +one central places. Importing `KEY_*`, `BTN_*`, etc. constants via `from evdev` is +fine. + Releasing --------- From ea52f85852f7369b264082de2544e4d2a8849076 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:04:09 +0200 Subject: [PATCH 05/32] don't you love it when things break overnight? --- tests/lib/patches.py | 10 +++++----- tests/new_test.py | 12 +++++++++--- tests/unit/test_injector.py | 4 ---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/lib/patches.py b/tests/lib/patches.py index 47ae6412c..7be860161 100644 --- a/tests/lib/patches.py +++ b/tests/lib/patches.py @@ -202,7 +202,7 @@ def input_props(self): uinputs = {} -class UInput: +class UInputMock: def __init__(self, events=None, name="unnamed", *args, **kwargs): self.fd = 0 self.write_count = 0 @@ -260,10 +260,10 @@ def copy(self): return [ patch.object(evdev, "list_devices", list_devices), patch.object(evdev, "InputDevice", InputDevice), - patch.object(evdev.UInput, "capabilities", UInput.capabilities), - patch.object(evdev.UInput, "write", UInput.write), - patch.object(evdev.UInput, "syn", UInput.syn), - patch.object(evdev.UInput, "__init__", UInput.__init__), + patch.object(evdev.UInput, "capabilities", UInputMock.capabilities), + patch.object(evdev.UInput, "write", UInputMock.write), + patch.object(evdev.UInput, "syn", UInputMock.syn), + patch.object(evdev.UInput, "__init__", UInputMock.__init__), patch.object(evdev, "InputEvent", PatchedInputEvent), ] diff --git a/tests/new_test.py b/tests/new_test.py index 73f4a5a5d..2f2a1c478 100644 --- a/tests/new_test.py +++ b/tests/new_test.py @@ -22,6 +22,7 @@ import os import subprocess +import traceback import tracemalloc from tests.lib.cleanup import cleanup, quick_cleanup @@ -91,6 +92,11 @@ def setUpClass(): create_fixture_pipes() + # I don't know. Somehow tearDownClass is called before the test, so lets + # make sure the patches are started already when the class is set up. + for patch in patches: + patch.start() + original_setUpClass() def tearDownClass(): @@ -98,9 +104,9 @@ def tearDownClass(): remove_fixture_pipes() - # Do the more thorough cleanup after all tests of classes, because it slows - # tests down. If this is required after each test, call it in your tearDown - # method. + # Do the more thorough cleanup only after all tests of classes, because it + # slows tests down. If this is required after each test, call it in your + # tearDown method. cleanup() def setUp(self): diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index dfb3e6b52..84ab72957 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -24,10 +24,6 @@ from pydantic import ValidationError from inputremapper.input_event import InputEvent -from tests.lib.global_uinputs import ( - reset_global_uinputs_for_service, - reset_global_uinputs_for_gui, -) from tests.lib.patches import uinputs from tests.lib.cleanup import quick_cleanup from tests.lib.constants import EVENT_READ_TIMEOUT From 891b4e1599da08e6229a646e313fe8f28536c29b Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:06:27 +0200 Subject: [PATCH 06/32] some mypy stuff --- inputremapper/configs/migrations.py | 2 +- inputremapper/logger/formatter.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index f5ca9dc56..d2ad3a3fc 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -344,7 +344,7 @@ def _input_combination_from_string(combination_string: str) -> InputCombination: return InputCombination(configs) @staticmethod - def _convert_to_individual_mappings(): + def _convert_to_individual_mappings() -> None: """Convert preset.json from {key: [symbol, target]} to [{input_combination: ..., output_symbol: symbol, ...}] diff --git a/inputremapper/logger/formatter.py b/inputremapper/logger/formatter.py index a11aa4582..67f0eb31a 100644 --- a/inputremapper/logger/formatter.py +++ b/inputremapper/logger/formatter.py @@ -38,7 +38,7 @@ def __init__(self, debug_mode: bool = False): super().__init__() self.debug_mode = debug_mode - self.file_color_mapping = {} + self.file_color_mapping: Dict[str, int] = {} # see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit self.allowed_colors = [] @@ -68,10 +68,10 @@ def __init__(self, debug_mode: bool = False): logging.FATAL: 9, } - def _get_ansi_code(self, r: int, g: int, b: int): + def _get_ansi_code(self, r: int, g: int, b: int) -> int: return 16 + b + (6 * g) + (36 * r) - def _word_to_color(self, word: str): + def _word_to_color(self, word: str) -> int: """Convert a word to a 8bit ansi color code.""" digit_sum = sum([ord(char) for char in word]) index = digit_sum % len(self.allowed_colors) From 448f028a49dc6a53891b1d512ec63d38ab7c32ae Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:11:45 +0200 Subject: [PATCH 07/32] ladjsfkla --- tests/new_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/new_test.py b/tests/new_test.py index 2f2a1c478..621a5eeb9 100644 --- a/tests/new_test.py +++ b/tests/new_test.py @@ -22,7 +22,6 @@ import os import subprocess -import traceback import tracemalloc from tests.lib.cleanup import cleanup, quick_cleanup @@ -93,7 +92,9 @@ def setUpClass(): create_fixture_pipes() # I don't know. Somehow tearDownClass is called before the test, so lets - # make sure the patches are started already when the class is set up. + # make sure the patches are started already when the class is set up, so that + # an unpatched `prepare_all` doesn't take ages to finish, and doesn't do funky + # stuff with the real evdev. for patch in patches: patch.start() From 8e4e510c750822a75b8d429d41588fe4a8202aaf Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:43:44 +0200 Subject: [PATCH 08/32] updated outdated assertion code or something --- tests/integration/test_components.py | 2 +- tests/integration/test_gui.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 0f61fe4aa..11cd7caee 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -270,7 +270,7 @@ def test_populates_devices(self): def test_updates_mapping(self): self.gui.set_active_id("baz") - self.controller_mock.update_mapping.called_once_with(target_uinput="baz") + self.controller_mock.update_mapping.assert_called_once_with(target_uinput="baz") def test_selects_correct_target(self): self.message_broker.publish(MappingData(target_uinput="baz")) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index 57bb39bd3..7ae71842e 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -492,10 +492,10 @@ def test_get_color_works(self): ) self.assertIsInstance(color, Gdk.RGBA) - self.assertNotAlmostEquals(color.red, fallback.red, delta=0.01) - self.assertNotAlmostEquals(color.green, fallback.blue, delta=0.01) - self.assertNotAlmostEquals(color.blue, fallback.green, delta=0.01) - self.assertNotAlmostEquals(color.alpha, fallback.alpha, delta=0.01) + self.assertNotAlmostEqual(color.red, fallback.red, delta=0.01) + self.assertNotAlmostEqual(color.green, fallback.blue, delta=0.01) + self.assertNotAlmostEqual(color.blue, fallback.green, delta=0.01) + self.assertNotAlmostEqual(color.alpha, fallback.alpha, delta=0.01) def _test_color_wont_fallback(self, get_color, fallback): color = get_color() From 703d3ef9d2791b4970ece93510b09a3140226b7d Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:30:49 +0200 Subject: [PATCH 09/32] aldjkf --- bin/input-remapper-gtk | 6 +- inputremapper/configs/global_config.py | 1 + inputremapper/configs/migrations.py | 6 +- inputremapper/configs/system_mapping.py | 1 + tests/integration/test_components.py | 2 +- tests/integration/test_daemon.py | 2 +- tests/integration/test_data.py | 2 +- tests/integration/test_gui.py | 4 +- tests/integration/test_numlock.py | 2 +- tests/integration/test_user_interface.py | 2 +- tests/legacy_test_setup.py | 182 ------------------ tests/{new_test.py => test.py} | 0 tests/unit/test_config.py | 2 +- tests/unit/test_context.py | 2 +- tests/unit/test_control.py | 2 +- tests/unit/test_controller.py | 2 +- tests/unit/test_daemon.py | 2 +- tests/unit/test_data_manager.py | 2 +- .../test_axis_transformation.py | 2 +- .../test_event_pipeline.py | 2 +- .../test_mapping_handlers.py | 2 +- tests/unit/test_event_reader.py | 2 +- tests/unit/test_global_uinputs.py | 2 +- tests/unit/test_groups.py | 2 +- tests/unit/test_injector.py | 2 +- tests/unit/test_input_config.py | 2 +- tests/unit/test_input_event.py | 2 +- tests/unit/test_ipc.py | 2 +- tests/unit/test_logger.py | 2 +- tests/unit/test_macros.py | 2 +- tests/unit/test_mapping.py | 2 +- tests/unit/test_message_broker.py | 2 +- tests/unit/test_migrations.py | 2 +- tests/unit/test_paths.py | 2 +- tests/unit/test_preset.py | 2 +- tests/unit/test_reader.py | 2 +- tests/unit/test_system_mapping.py | 2 +- tests/unit/test_test.py | 2 +- tests/unit/test_user.py | 2 +- tests/unit/test_util.py | 2 +- 40 files changed, 45 insertions(+), 221 deletions(-) delete mode 100644 tests/legacy_test_setup.py rename tests/{new_test.py => test.py} (100%) diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index 5ea4493d1..95e9fa38b 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -70,6 +70,8 @@ if __name__ == "__main__": logger.log_info("input-remapper-gtk") logger.debug("Using locale directory: {}".format(LOCALE_DIR)) + # TODO no. Importing files shouldn't run any code, so it shouldn't matter when the + # log-level is set. # import input-remapper stuff after setting the log verbosity from inputremapper.gui.messages.message_broker import MessageBroker, MessageType from inputremapper.configs.system_mapping import system_mapping @@ -81,9 +83,9 @@ if __name__ == "__main__": from inputremapper.gui.reader_client import ReaderClient from inputremapper.daemon import Daemon from inputremapper.configs.global_config import GlobalConfig - from inputremapper.configs.migrations import migrate + from inputremapper.configs.migrations import Migrations - migrate() + Migrations.migrate() message_broker = MessageBroker() diff --git a/inputremapper/configs/global_config.py b/inputremapper/configs/global_config.py index 852a98adb..6c94ef2c6 100644 --- a/inputremapper/configs/global_config.py +++ b/inputremapper/configs/global_config.py @@ -131,4 +131,5 @@ def _save_config(self): file.write("\n") +# TODO DI global_config = GlobalConfig() diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index d2ad3a3fc..bb1e5a638 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -19,7 +19,7 @@ """Migration functions. -Only write changes to disk, if there actually are changes. Otherwise file-modification +Only write changes to disk, if there actually are changes. Otherwise, file-modification dates are destroyed. """ from __future__ import annotations @@ -51,8 +51,8 @@ from inputremapper.configs.mapping import Mapping, UIMapping from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.configs.system_mapping import system_mapping, SystemMapping +from inputremapper.injection.global_uinputs import global_uinputs, GlobalUInputs from inputremapper.injection.macros.parse import is_this_a_macro from inputremapper.logger.logger import logger, VERSION from inputremapper.user import UserUtils diff --git a/inputremapper/configs/system_mapping.py b/inputremapper/configs/system_mapping.py index d84e234f7..2a3677e09 100644 --- a/inputremapper/configs/system_mapping.py +++ b/inputremapper/configs/system_mapping.py @@ -214,5 +214,6 @@ def _find_legit_mappings(self) -> dict: return xmodmap_dict +# TODO DI # this mapping represents the xmodmap output, which stays constant system_mapping = SystemMapping() diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 11cd7caee..1da294d7e 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -86,7 +86,7 @@ ) from inputremapper.configs.mapping import MappingData from inputremapper.configs.input_config import InputCombination, InputConfig -from tests.new_test import setup_tests +from tests.test import setup_tests class ComponentBaseTest(unittest.TestCase): diff --git a/tests/integration/test_daemon.py b/tests/integration/test_daemon.py index 13507b165..fd478fb22 100644 --- a/tests/integration/test_daemon.py +++ b/tests/integration/test_daemon.py @@ -30,7 +30,7 @@ from gi.repository import Gtk from inputremapper.daemon import Daemon, BUS_NAME -from tests.new_test import setup_tests +from tests.test import setup_tests def gtk_iteration(): diff --git a/tests/integration/test_data.py b/tests/integration/test_data.py index f0f66b5fe..949c504ae 100644 --- a/tests/integration/test_data.py +++ b/tests/integration/test_data.py @@ -24,7 +24,7 @@ import pkg_resources from inputremapper.configs.data import get_data_path -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index 7ae71842e..f515bed86 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -20,7 +20,9 @@ import asyncio +# TODO no: # the tests file needs to be imported first to make sure patches are loaded +# TODO "optimize imports" of all files from tests.test import get_project_root from contextlib import contextmanager @@ -94,7 +96,7 @@ from inputremapper.injection.injector import InjectorState from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.daemon import Daemon, DaemonProxy -from tests.new_test import setup_tests +from tests.test import setup_tests # iterate a few times when Gtk.main() is called, but don't block diff --git a/tests/integration/test_numlock.py b/tests/integration/test_numlock.py index 30061d76b..b65bbdb27 100644 --- a/tests/integration/test_numlock.py +++ b/tests/integration/test_numlock.py @@ -24,7 +24,7 @@ import unittest from inputremapper.injection.numlock import is_numlock_on, set_numlock, ensure_numlock -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/integration/test_user_interface.py b/tests/integration/test_user_interface.py index 32fb38fb9..2b7862637 100644 --- a/tests/integration/test_user_interface.py +++ b/tests/integration/test_user_interface.py @@ -16,7 +16,7 @@ from inputremapper.gui.user_interface import UserInterface from inputremapper.configs.mapping import MappingData from inputremapper.configs.input_config import InputCombination, InputConfig -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/legacy_test_setup.py b/tests/legacy_test_setup.py deleted file mode 100644 index 0247e5eaa..000000000 --- a/tests/legacy_test_setup.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -# input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb -# -# This file is part of input-remapper. -# -# input-remapper is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# input-remapper is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with input-remapper. If not, see . - -from __future__ import annotations - -import argparse -import os -import sys -import tracemalloc -import traceback - -traceback.print_stack() - -# TODO don't, the decorator should handle it: -tracemalloc.start() - -# TODO this won't be required anymore: -# ensure nothing has loaded -if module := sys.modules.get("inputremapper"): - imported = [m for m in module.__dict__ if not m.startswith("__")] - raise AssertionError( - f"The modules {imported} from inputremapper where already imported, this can " - f"cause issues with the tests. Make sure to always import tests.test before any" - f" inputremapper module." - ) -try: - sys.modules.get("tests.test").main - raise AssertionError( - "test.py was already imported. " - "Always use 'from tests.test import ...' " - "not 'from test import ...' to import this" - ) - # have fun debugging infinitely blocking tests without this -except AttributeError: - pass - - -def get_project_root(): - """Find the projects root, i.e. the uppermost directory of the repo.""" - # when tests are started in pycharm via the green arrow, the working directory - # is not the project root. Go up until it is found. - root = os.getcwd() - for _ in range(10): - if "setup.py" in os.listdir(root): - return root - - root = os.path.dirname(root) - - raise Exception("Could not find project root") - - -# TODO no, it should be installed locally so that it can be imported properly if really -# needed, instead of doing hacky sys.path stuff -# make sure the "tests" module visible -sys.path.append(get_project_root()) -if __name__ == "__main__": - # import this file to itself to make sure is not run twice and all global - # variables end up in sys.modules - # https://stackoverflow.com/questions/13181559/importing-modules-main-vs-import-as-module - import tests.test - - tests.test.main() - -import unittest -import subprocess - -# TODO don't, the decorator should handle it: -os.environ["UNITTEST"] = "1" - -from tests.lib.fixtures import fixtures -from tests.lib.pipes import setup_pipe, close_pipe -from tests.lib.patches import apply_all_patches -from tests.lib.cleanup import cleanup -from tests.lib.logger import update_inputremapper_verbosity - - -def is_service_running(): - """Check if the daemon is running.""" - try: - subprocess.check_output(["pgrep", "-f", "input-remapper-service"]) - return True - except subprocess.CalledProcessError: - return False - - -# TODO don't, the decorator should handle it: -if is_service_running(): - # let tests control daemon existance - raise Exception("Expected the service not to be running already.") - - -def create_fixture_pipes(): - # make sure those pipes exist before any process (the reader-service) gets forked, - # so that events can be pushed after the fork. - for _fixture in fixtures: - setup_pipe(_fixture) - - -def remove_fixture_pipes(): - for _fixture in fixtures: - close_pipe(_fixture) - - -# TODO don't, the decorator should handle it: -create_fixture_pipes() - - -# applying patches before importing input-remappers modules is important, otherwise -# input-remapper might use non-patched modules. Importing modules from inputremapper -# just-in-time in the test-setup functions instead of globally helps. This way, -# it is ensured that the patches on evdev and such are already applied, without having -# to take care about ordering the files in a special way. -# TODO -# 1. The above is why we should wrap everything in classes (including libraries like -# evdev that don't follow that pattern) and then use dependency-injection. Some work -# on this has been done already, but more is needed. -# 2. Apply the patches in beforeEach (or a pytest autouse fixture) -# - By doing those two things, we might get a test-setup that works automatically -# correctly in IDEs. This is horrible. And it has been even more horrible in the past. -# -# TODO don't, the decorator should handle it: -apply_all_patches() - -# TODO don't, the decorator should handle it: -update_inputremapper_verbosity() - - -# TODO this shouldn't be needed really. Either use pytest or unittest cli to run -# tests, or IDE tools. -def main(): - cleanup() - # https://docs.python.org/3/library/argparse.html - parser = argparse.ArgumentParser(description=__doc__) - # repeated argument 0 or more times with modules - parser.add_argument("modules", type=str, nargs="*") - # start-dir value if not using modules, allows eg python tests/test.py --start-dir unit - parser.add_argument("--start-dir", type=str, default=".") - parsed_args = parser.parse_args() # takes from sys.argv by default - modules = parsed_args.modules - - # discoverer is really convenient, but it can't find a specific test - # in all of the available tests like unittest.main() does..., - # so provide both options. - if len(modules) > 0: - # for example - # `tests/test.py integration.test_gui.TestGui.test_can_start` - # or `tests/test.py integration.test_gui integration.test_daemon` - testsuite = unittest.defaultTestLoader.loadTestsFromNames(modules) - else: - # run all tests by default - testsuite = unittest.defaultTestLoader.discover( - parsed_args.start_dir, pattern="test_*.py" - ) - - # add a newline to each "qux (foo.bar)..." output before each test, - # because the first log will be on the same line otherwise - original_start_test = unittest.TextTestResult.startTest - - def start_test(self, test): - original_start_test(self, test) - print() - - unittest.TextTestResult.startTest = start_test - result = unittest.TextTestRunner(verbosity=2).run(testsuite) - sys.exit(not result.wasSuccessful()) diff --git a/tests/new_test.py b/tests/test.py similarity index 100% rename from tests/new_test.py rename to tests/test.py diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 83332d5a0..e1292f713 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -27,7 +27,7 @@ from tests.lib.cleanup import quick_cleanup from tests.lib.tmp import tmp -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index f3e1ed015..df0d14de6 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -33,7 +33,7 @@ from inputremapper.configs.preset import Preset from inputremapper.configs.mapping import Mapping from inputremapper.configs.input_config import InputCombination -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index 2471fe9bf..6fa303dc5 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -37,7 +37,7 @@ from inputremapper.configs.preset import Preset from inputremapper.configs.paths import PathUtils from inputremapper.groups import groups -from tests.new_test import setup_tests +from tests.test import setup_tests def import_control(): diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index 8f640d7af..1d6f91f10 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -62,7 +62,7 @@ from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index 25b3baa2b..7ed1aea70 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -47,7 +47,7 @@ from inputremapper.injection.injector import InjectorState from inputremapper.daemon import Daemon from inputremapper.injection.global_uinputs import global_uinputs -from tests.new_test import setup_tests, is_service_running +from tests.test import setup_tests, is_service_running check_output = subprocess.check_output os_system = os.system diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index ef3e63b76..f3433a0b5 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -47,7 +47,7 @@ from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME -from tests.new_test import setup_tests +from tests.test import setup_tests class Listener: diff --git a/tests/unit/test_event_pipeline/test_axis_transformation.py b/tests/unit/test_event_pipeline/test_axis_transformation.py index 1b5b8acc5..93ad09253 100644 --- a/tests/unit/test_event_pipeline/test_axis_transformation.py +++ b/tests/unit/test_event_pipeline/test_axis_transformation.py @@ -24,7 +24,7 @@ from typing import Iterable, List from inputremapper.injection.mapping_handlers.axis_transform import Transformation -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_event_pipeline/test_event_pipeline.py b/tests/unit/test_event_pipeline/test_event_pipeline.py index e693b44d9..9015c8a79 100644 --- a/tests/unit/test_event_pipeline/test_event_pipeline.py +++ b/tests/unit/test_event_pipeline/test_event_pipeline.py @@ -63,7 +63,7 @@ from tests.lib.logger import logger from tests.lib.constants import MAX_ABS, MIN_ABS from tests.lib.fixtures import Fixture, fixtures -from tests.new_test import setup_tests +from tests.test import setup_tests class EventPipelineTestBase(unittest.IsolatedAsyncioTestCase): diff --git a/tests/unit/test_event_pipeline/test_mapping_handlers.py b/tests/unit/test_event_pipeline/test_mapping_handlers.py index 0bce2d7fe..7b799c77d 100644 --- a/tests/unit/test_event_pipeline/test_mapping_handlers.py +++ b/tests/unit/test_event_pipeline/test_mapping_handlers.py @@ -67,7 +67,7 @@ from tests.lib.patches import InputDevice from tests.lib.constants import MAX_ABS from tests.lib.fixtures import fixtures -from tests.new_test import setup_tests +from tests.test import setup_tests class BaseTests: diff --git a/tests/unit/test_event_reader.py b/tests/unit/test_event_reader.py index 51e20621c..a9223ef14 100644 --- a/tests/unit/test_event_reader.py +++ b/tests/unit/test_event_reader.py @@ -47,7 +47,7 @@ from inputremapper.utils import get_device_hash from tests.lib.fixtures import fixtures from tests.lib.cleanup import quick_cleanup -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_global_uinputs.py b/tests/unit/test_global_uinputs.py index 61323609a..e0793b452 100644 --- a/tests/unit/test_global_uinputs.py +++ b/tests/unit/test_global_uinputs.py @@ -38,7 +38,7 @@ GlobalUInputs, ) from inputremapper.exceptions import EventNotHandled, UinputNotAvailable -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_groups.py b/tests/unit/test_groups.py index b03a58c00..06e49d7cf 100644 --- a/tests/unit/test_groups.py +++ b/tests/unit/test_groups.py @@ -37,7 +37,7 @@ DeviceType, _Group, ) -from tests.new_test import setup_tests +from tests.test import setup_tests class FakePipe: diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index 84ab72957..87c3267c3 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -67,7 +67,7 @@ from inputremapper.injection.macros.parse import parse from inputremapper.injection.context import Context from inputremapper.groups import groups, classify, DeviceType -from tests.new_test import setup_tests +from tests.test import setup_tests def wait_for_uinput_write(): diff --git a/tests/unit/test_input_config.py b/tests/unit/test_input_config.py index c8b096c81..97d62170d 100644 --- a/tests/unit/test_input_config.py +++ b/tests/unit/test_input_config.py @@ -44,7 +44,7 @@ ) from inputremapper.configs.input_config import InputCombination, InputConfig -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_input_event.py b/tests/unit/test_input_event.py index 81b28a43e..7f3621c3c 100644 --- a/tests/unit/test_input_event.py +++ b/tests/unit/test_input_event.py @@ -23,7 +23,7 @@ import evdev from dataclasses import FrozenInstanceError from inputremapper.input_event import InputEvent -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_ipc.py b/tests/unit/test_ipc.py index 098a74b62..8d9712269 100644 --- a/tests/unit/test_ipc.py +++ b/tests/unit/test_ipc.py @@ -31,7 +31,7 @@ from inputremapper.ipc.pipe import Pipe from inputremapper.ipc.shared_dict import SharedDict from inputremapper.ipc.socket import Server, Client, Base -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index 564fdcd8a..5b024cdfe 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -32,7 +32,7 @@ logger, ColorfulFormatter, ) -from tests.new_test import setup_tests +from tests.test import setup_tests def add_filehandler(log_path: str, debug: bool) -> None: diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index 1838f0510..9a750da22 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -71,7 +71,7 @@ from inputremapper.input_event import InputEvent from tests.lib.logger import logger from tests.lib.cleanup import quick_cleanup -from tests.new_test import setup_tests +from tests.test import setup_tests class MacroTestBase(unittest.IsolatedAsyncioTestCase): diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py index 55e5f4496..bcfa7ffdd 100644 --- a/tests/unit/test_mapping.py +++ b/tests/unit/test_mapping.py @@ -40,7 +40,7 @@ from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.gui.messages.message_broker import MessageType -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_message_broker.py b/tests/unit/test_message_broker.py index d0ec410d2..c58ebf4bf 100644 --- a/tests/unit/test_message_broker.py +++ b/tests/unit/test_message_broker.py @@ -22,7 +22,7 @@ from dataclasses import dataclass from inputremapper.gui.messages.message_broker import MessageBroker, MessageType, Signal -from tests.new_test import setup_tests +from tests.test import setup_tests class Listener: diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index 5db3157b6..59904f32e 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -44,7 +44,7 @@ from inputremapper.configs.preset import Preset from inputremapper.logger.logger import VERSION from inputremapper.user import UserUtils -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py index b54caf0c5..f4959685b 100644 --- a/tests/unit/test_paths.py +++ b/tests/unit/test_paths.py @@ -27,7 +27,7 @@ import tempfile from inputremapper.configs.paths import PathUtils -from tests.new_test import setup_tests +from tests.test import setup_tests def _raise(error): diff --git a/tests/unit/test_preset.py b/tests/unit/test_preset.py index d03d34066..3a57cae99 100644 --- a/tests/unit/test_preset.py +++ b/tests/unit/test_preset.py @@ -30,7 +30,7 @@ from inputremapper.configs.preset import Preset from inputremapper.configs.input_config import InputCombination, InputConfig from tests.lib.cleanup import quick_cleanup -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index fdf433c3d..fe05cea04 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -64,7 +64,7 @@ from tests.lib.pipes import push_event, push_events from tests.lib.fixtures import fixtures from tests.lib.stuff import spy -from tests.new_test import setup_tests +from tests.test import setup_tests CODE_1 = 100 CODE_2 = 101 diff --git a/tests/unit/test_system_mapping.py b/tests/unit/test_system_mapping.py index 8121c730c..b9d6ed809 100644 --- a/tests/unit/test_system_mapping.py +++ b/tests/unit/test_system_mapping.py @@ -29,7 +29,7 @@ from inputremapper.configs.paths import PathUtils from inputremapper.configs.system_mapping import SystemMapping, XMODMAP_FILENAME from tests.lib.cleanup import quick_cleanup -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_test.py b/tests/unit/test_test.py index e11169465..227f91b6e 100644 --- a/tests/unit/test_test.py +++ b/tests/unit/test_test.py @@ -40,7 +40,7 @@ from inputremapper.input_event import InputEvent from inputremapper.utils import get_device_hash from inputremapper.gui.messages.message_broker import MessageBroker -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests diff --git a/tests/unit/test_user.py b/tests/unit/test_user.py index c9bae70b7..dcd31aa55 100644 --- a/tests/unit/test_user.py +++ b/tests/unit/test_user.py @@ -26,7 +26,7 @@ from unittest import mock from inputremapper.user import UserUtils -from tests.new_test import setup_tests +from tests.test import setup_tests def _raise(error): diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 71648a5ed..5c11ac44a 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -24,7 +24,7 @@ from evdev._ecodes import EV_ABS, ABS_X, BTN_WEST, BTN_Y, EV_KEY, KEY_A from inputremapper.utils import get_evdev_constant_name -from tests.new_test import setup_tests +from tests.test import setup_tests @setup_tests From d8b99954da2c81c6997961a0d69c4e050b2cd4dd Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:18:08 +0200 Subject: [PATCH 10/32] optimize imports --- bin/input-remapper-reader-service | 4 +- bin/input-remapper-service | 6 +- inputremapper/gui/autocompletion.py | 2 +- inputremapper/gui/components/main.py | 1 - inputremapper/gui/controller.py | 4 +- inputremapper/gui/data_manager.py | 5 +- inputremapper/gui/reader_client.py | 20 +++---- inputremapper/gui/reader_service.py | 2 +- inputremapper/gui/user_interface.py | 12 ++-- inputremapper/gui/utils.py | 3 - inputremapper/injection/context.py | 2 +- inputremapper/injection/event_reader.py | 2 +- .../mapping_handlers/abs_to_abs_handler.py | 2 +- .../mapping_handlers/abs_to_btn_handler.py | 1 - .../mapping_handlers/axis_switch_handler.py | 4 +- .../mapping_handlers/axis_transform.py | 1 + .../mapping_handlers/combination_handler.py | 1 + .../mapping_handlers/hierarchy_handler.py | 1 + .../injection/mapping_handlers/key_handler.py | 2 +- .../mapping_handlers/rel_to_abs_handler.py | 2 +- .../mapping_handlers/rel_to_rel_handler.py | 2 +- tests/integration/test_components.py | 5 +- tests/integration/test_gui.py | 55 ++++++++----------- 23 files changed, 61 insertions(+), 78 deletions(-) diff --git a/bin/input-remapper-reader-service b/bin/input-remapper-reader-service index e82b4f1e5..0a5473c5a 100755 --- a/bin/input-remapper-reader-service +++ b/bin/input-remapper-reader-service @@ -28,7 +28,7 @@ import sys from argparse import ArgumentParser from inputremapper.groups import _Groups -from inputremapper.logger.logger import update_verbosity +from inputremapper.logger.logger import logger if __name__ == "__main__": parser = ArgumentParser() @@ -43,7 +43,7 @@ if __name__ == "__main__": options = parser.parse_args(sys.argv[1:]) - update_verbosity(options.debug) + logger.update_verbosity(options.debug) # import input-remapper stuff after setting the log verbosity from inputremapper.gui.reader_service import ReaderService diff --git a/bin/input-remapper-service b/bin/input-remapper-service index c5fdd269e..bfaf9799e 100755 --- a/bin/input-remapper-service +++ b/bin/input-remapper-service @@ -25,7 +25,7 @@ import sys from argparse import ArgumentParser -from inputremapper.logger.logger import update_verbosity, log_info +from inputremapper.logger.logger import logger if __name__ == "__main__": parser = ArgumentParser() @@ -47,13 +47,13 @@ if __name__ == "__main__": options = parser.parse_args(sys.argv[1:]) - update_verbosity(options.debug) + logger.update_verbosity(options.debug) # import input-remapper stuff after setting the log verbosity from inputremapper.daemon import Daemon if not options.hide_info: - log_info("input-remapper-service") + logger.log_info("input-remapper-service") daemon = Daemon() daemon.publish() diff --git a/inputremapper/gui/autocompletion.py b/inputremapper/gui/autocompletion.py index c7e8feb85..9ff7dd96f 100644 --- a/inputremapper/gui/autocompletion.py +++ b/inputremapper/gui/autocompletion.py @@ -27,10 +27,10 @@ from evdev.ecodes import EV_KEY from gi.repository import Gdk, Gtk, GLib, GObject -from inputremapper.gui.controller import Controller from inputremapper.configs.mapping import MappingData from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME from inputremapper.gui.components.editor import CodeEditor +from inputremapper.gui.controller import Controller from inputremapper.gui.messages.message_broker import MessageBroker, MessageType from inputremapper.gui.messages.message_data import UInputsData from inputremapper.gui.utils import debounce diff --git a/inputremapper/gui/components/main.py b/inputremapper/gui/components/main.py index ec3544580..6d21d8a0b 100644 --- a/inputremapper/gui/components/main.py +++ b/inputremapper/gui/components/main.py @@ -23,7 +23,6 @@ from __future__ import annotations -import gi from gi.repository import Gtk, Pango from inputremapper.gui.controller import Controller diff --git a/inputremapper/gui/controller.py b/inputremapper/gui/controller.py index d44c4d572..e03c782d9 100644 --- a/inputremapper/gui/controller.py +++ b/inputremapper/gui/controller.py @@ -33,11 +33,10 @@ Any, ) -import gi from evdev.ecodes import EV_KEY, EV_REL, EV_ABS - from gi.repository import Gtk +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import ( MappingData, UIMapping, @@ -48,7 +47,6 @@ OutputSymbolVariantError, ) from inputremapper.configs.paths import PathUtils -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.validation_errors import pydantify from inputremapper.exceptions import DataManagementError from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME diff --git a/inputremapper/gui/data_manager.py b/inputremapper/gui/data_manager.py index 29bfeb171..5606ecb1b 100644 --- a/inputremapper/gui/data_manager.py +++ b/inputremapper/gui/data_manager.py @@ -23,19 +23,18 @@ import time from typing import Optional, List, Tuple, Set -import gi from gi.repository import GLib from inputremapper.configs.global_config import GlobalConfig +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import UIMapping, MappingData from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.configs.system_mapping import SystemMapping from inputremapper.daemon import DaemonProxy -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.exceptions import DataManagementError -from inputremapper.gui.gettext import _ from inputremapper.groups import _Group +from inputremapper.gui.gettext import _ from inputremapper.gui.messages.message_broker import ( MessageBroker, ) diff --git a/inputremapper/gui/reader_client.py b/inputremapper/gui/reader_client.py index 2dae072b4..d392232ac 100644 --- a/inputremapper/gui/reader_client.py +++ b/inputremapper/gui/reader_client.py @@ -24,15 +24,21 @@ """ import time -from typing import Optional, List, Generator, Dict, Tuple, Set +from typing import Optional, List, Generator, Dict, Set import evdev -import gi - from gi.repository import GLib from inputremapper.configs.input_config import InputCombination from inputremapper.groups import _Groups, _Group +from inputremapper.gui.gettext import _ +from inputremapper.gui.messages.message_broker import MessageBroker +from inputremapper.gui.messages.message_data import ( + GroupsData, + CombinationRecorded, + StatusData, +) +from inputremapper.gui.messages.message_types import MessageType from inputremapper.gui.reader_service import ( MSG_EVENT, MSG_GROUPS, @@ -42,15 +48,7 @@ get_pipe_paths, ReaderService, ) -from inputremapper.gui.messages.message_types import MessageType -from inputremapper.gui.messages.message_broker import MessageBroker -from inputremapper.gui.messages.message_data import ( - GroupsData, - CombinationRecorded, - StatusData, -) from inputremapper.gui.utils import CTX_ERROR -from inputremapper.gui.gettext import _ from inputremapper.input_event import InputEvent from inputremapper.ipc.pipe import Pipe from inputremapper.logger.logger import logger diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index 510936151..d8247eec9 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -50,7 +50,6 @@ import evdev from evdev.ecodes import EV_KEY, EV_ABS, EV_REL, REL_HWHEEL, REL_WHEEL -from inputremapper.utils import get_device_hash from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping @@ -67,6 +66,7 @@ from inputremapper.ipc.pipe import Pipe from inputremapper.logger.logger import logger from inputremapper.user import UserUtils +from inputremapper.utils import get_device_hash # received by the reader-service CMD_TERMINATE = "terminate" diff --git a/inputremapper/gui/user_interface.py b/inputremapper/gui/user_interface.py index 54235eae3..83ff636a6 100644 --- a/inputremapper/gui/user_interface.py +++ b/inputremapper/gui/user_interface.py @@ -21,14 +21,14 @@ """User Interface.""" from typing import Dict, Callable -import gi - from gi.repository import Gtk, GtkSource, Gdk, GObject from inputremapper.configs.data import get_data_path -from inputremapper.configs.mapping import MappingData from inputremapper.configs.input_config import InputCombination +from inputremapper.configs.mapping import MappingData from inputremapper.gui.autocompletion import Autocompletion +from inputremapper.gui.components.common import Breadcrumbs +from inputremapper.gui.components.device_groups import DeviceGroupSelection from inputremapper.gui.components.editor import ( MappingListBox, TargetSelection, @@ -49,11 +49,10 @@ RequireActiveMapping, GdkEventRecorder, ) -from inputremapper.gui.components.presets import PresetSelection from inputremapper.gui.components.main import Stack, StatusBar -from inputremapper.gui.components.common import Breadcrumbs -from inputremapper.gui.components.device_groups import DeviceGroupSelection +from inputremapper.gui.components.presets import PresetSelection from inputremapper.gui.controller import Controller +from inputremapper.gui.gettext import _ from inputremapper.gui.messages.message_broker import ( MessageBroker, MessageType, @@ -64,7 +63,6 @@ ) from inputremapper.injection.injector import InjectorStateMessage from inputremapper.logger.logger import logger, COMMIT_HASH, VERSION, EVDEV_VERSION -from inputremapper.gui.gettext import _ # https://cjenkins.wordpress.com/2012/05/08/use-gtksourceview-widget-in-glade/ GObject.type_register(GtkSource.View) diff --git a/inputremapper/gui/utils.py b/inputremapper/gui/utils.py index be3ef3ce0..0af377e38 100644 --- a/inputremapper/gui/utils.py +++ b/inputremapper/gui/utils.py @@ -23,13 +23,10 @@ from dataclasses import dataclass from typing import List, Callable, Dict, Optional -import gi - from gi.repository import Gtk, GLib, Gdk from inputremapper.logger.logger import logger - # status ctx ids CTX_SAVE = 0 diff --git a/inputremapper/injection/context.py b/inputremapper/injection/context.py index 11a324332..5e4c196b8 100644 --- a/inputremapper/injection/context.py +++ b/inputremapper/injection/context.py @@ -28,7 +28,6 @@ import evdev from inputremapper.configs.input_config import DeviceHash -from inputremapper.input_event import InputEvent from inputremapper.configs.preset import Preset from inputremapper.injection.mapping_handlers.mapping_handler import ( EventListener, @@ -38,6 +37,7 @@ parse_mappings, EventPipelines, ) +from inputremapper.input_event import InputEvent from inputremapper.logger.logger import logger diff --git a/inputremapper/injection/event_reader.py b/inputremapper/injection/event_reader.py index 33a702be4..d125afc01 100644 --- a/inputremapper/injection/event_reader.py +++ b/inputremapper/injection/event_reader.py @@ -27,13 +27,13 @@ import evdev -from inputremapper.utils import get_device_hash, DeviceHash from inputremapper.injection.mapping_handlers.mapping_handler import ( EventListener, NotifyCallback, ) from inputremapper.input_event import InputEvent from inputremapper.logger.logger import logger +from inputremapper.utils import get_device_hash, DeviceHash class Context(Protocol): diff --git a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py index 51f86de97..576c80b36 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py @@ -22,8 +22,8 @@ import evdev from evdev.ecodes import EV_ABS -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper import exceptions +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.injection.global_uinputs import global_uinputs from inputremapper.injection.mapping_handlers.axis_transform import Transformation diff --git a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py index 136ed4baf..123cd22bf 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . - from typing import Tuple import evdev diff --git a/inputremapper/injection/mapping_handlers/axis_switch_handler.py b/inputremapper/injection/mapping_handlers/axis_switch_handler.py index 13cfa4c4c..68a53f117 100644 --- a/inputremapper/injection/mapping_handlers/axis_switch_handler.py +++ b/inputremapper/injection/mapping_handlers/axis_switch_handler.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from typing import Dict, Tuple, Hashable, TYPE_CHECKING +from typing import Dict, Tuple, Hashable import evdev -from inputremapper.configs.input_config import InputConfig from inputremapper.configs.input_config import InputCombination +from inputremapper.configs.input_config import InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, diff --git a/inputremapper/injection/mapping_handlers/axis_transform.py b/inputremapper/injection/mapping_handlers/axis_transform.py index 0460fc36f..a81be65c2 100644 --- a/inputremapper/injection/mapping_handlers/axis_transform.py +++ b/inputremapper/injection/mapping_handlers/axis_transform.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + import math from typing import Dict, Union diff --git a/inputremapper/injection/mapping_handlers/combination_handler.py b/inputremapper/injection/mapping_handlers/combination_handler.py index 7e8dcc3e3..0a3d2018b 100644 --- a/inputremapper/injection/mapping_handlers/combination_handler.py +++ b/inputremapper/injection/mapping_handlers/combination_handler.py @@ -18,6 +18,7 @@ # along with input-remapper. If not, see . from __future__ import annotations # needed for the TYPE_CHECKING import + from typing import TYPE_CHECKING, Dict, Hashable import evdev diff --git a/inputremapper/injection/mapping_handlers/hierarchy_handler.py b/inputremapper/injection/mapping_handlers/hierarchy_handler.py index 13d09b720..cf611cca8 100644 --- a/inputremapper/injection/mapping_handlers/hierarchy_handler.py +++ b/inputremapper/injection/mapping_handlers/hierarchy_handler.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + from typing import List, Dict import evdev diff --git a/inputremapper/injection/mapping_handlers/key_handler.py b/inputremapper/injection/mapping_handlers/key_handler.py index 493e41987..f1ae0d62d 100644 --- a/inputremapper/injection/mapping_handlers/key_handler.py +++ b/inputremapper/injection/mapping_handlers/key_handler.py @@ -19,8 +19,8 @@ from typing import Tuple, Dict -from inputremapper.configs.input_config import InputCombination from inputremapper import exceptions +from inputremapper.configs.input_config import InputCombination from inputremapper.configs.mapping import Mapping from inputremapper.exceptions import MappingParsingError from inputremapper.injection.global_uinputs import global_uinputs diff --git a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py index 945439c94..10b5e11cb 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py @@ -30,8 +30,8 @@ REL_WHEEL_HI_RES, ) -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper import exceptions +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import ( Mapping, WHEEL_SCALING, diff --git a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py index c9837ac9b..22ea6f1f3 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py @@ -29,8 +29,8 @@ REL_HWHEEL_HI_RES, ) -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper import exceptions +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import ( Mapping, REL_XY_SCALING, diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 1da294d7e..cfc434383 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -18,15 +18,14 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . +import time import unittest from typing import Optional, Tuple, Union from unittest.mock import MagicMock, call -import time import evdev -from evdev.ecodes import KEY_A, KEY_B, KEY_C - import gi +from evdev.ecodes import KEY_A, KEY_B, KEY_C gi.require_version("Gdk", "3.0") gi.require_version("Gtk", "3.0") diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index f515bed86..d08464e17 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -19,37 +19,20 @@ # along with input-remapper. If not, see . import asyncio - -# TODO no: -# the tests file needs to be imported first to make sure patches are loaded -# TODO "optimize imports" of all files -from tests.test import get_project_root - -from contextlib import contextmanager -from typing import Tuple, List, Optional, Iterable - -from inputremapper.gui.autocompletion import ( - get_incomplete_parameter, - get_incomplete_function_name, -) - -from tests.lib.global_uinputs import reset_global_uinputs_for_service -from tests.lib.cleanup import cleanup -from tests.lib.stuff import spy -from tests.lib.constants import EVENT_READ_TIMEOUT -from tests.lib.fixtures import prepare_presets -from tests.lib.logger import logger -from tests.lib.fixtures import fixtures -from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe -from tests.integration.test_components import FlowBoxTestUtils - -import sys -import time import atexit +import multiprocessing import os +import sys +import time import unittest -import multiprocessing +from contextlib import contextmanager +from importlib.machinery import SourceFileLoader +from importlib.util import spec_from_loader, module_from_spec +from typing import Tuple, List, Optional, Iterable +from unittest.mock import patch, MagicMock, call + import evdev +import gi from evdev.ecodes import ( EV_KEY, EV_ABS, @@ -58,13 +41,23 @@ KEY_Q, EV_REL, ) -from unittest.mock import patch, MagicMock, call -from importlib.util import spec_from_loader, module_from_spec -from importlib.machinery import SourceFileLoader +from inputremapper.gui.autocompletion import ( + get_incomplete_parameter, + get_incomplete_function_name, +) from inputremapper.input_event import InputEvent +from tests.integration.test_components import FlowBoxTestUtils +from tests.lib.cleanup import cleanup +from tests.lib.constants import EVENT_READ_TIMEOUT +from tests.lib.fixtures import fixtures +from tests.lib.fixtures import prepare_presets +from tests.lib.global_uinputs import reset_global_uinputs_for_service +from tests.lib.logger import logger +from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe +from tests.lib.stuff import spy -import gi +from tests.test import get_project_root gi.require_version("Gdk", "3.0") gi.require_version("Gtk", "3.0") From 52b02107c5b1e75b1a1633752a4382845f9c0ccb Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:25:59 +0200 Subject: [PATCH 11/32] update copyright year, refactoring test lib --- bin/input-remapper-control | 2 +- bin/input-remapper-gtk | 2 +- bin/input-remapper-reader-service | 2 +- bin/input-remapper-service | 2 +- inputremapper/configs/base_config.py | 2 +- inputremapper/configs/data.py | 2 +- inputremapper/configs/global_config.py | 2 +- inputremapper/configs/input_config.py | 2 +- inputremapper/configs/mapping.py | 2 +- inputremapper/configs/migrations.py | 2 +- inputremapper/configs/paths.py | 2 +- inputremapper/configs/preset.py | 2 +- inputremapper/configs/system_mapping.py | 2 +- inputremapper/configs/validation_errors.py | 2 +- inputremapper/daemon.py | 2 +- inputremapper/exceptions.py | 2 +- inputremapper/groups.py | 2 +- inputremapper/gui/autocompletion.py | 2 +- inputremapper/gui/components/common.py | 2 +- inputremapper/gui/components/device_groups.py | 2 +- inputremapper/gui/components/editor.py | 2 +- inputremapper/gui/components/main.py | 2 +- inputremapper/gui/components/presets.py | 2 +- inputremapper/gui/controller.py | 2 +- inputremapper/gui/data_manager.py | 2 +- inputremapper/gui/gettext.py | 2 +- inputremapper/gui/messages/message_broker.py | 2 +- inputremapper/gui/messages/message_data.py | 2 +- inputremapper/gui/messages/message_types.py | 2 +- inputremapper/gui/reader_client.py | 2 +- inputremapper/gui/user_interface.py | 2 +- inputremapper/gui/utils.py | 2 +- inputremapper/injection/context.py | 2 +- inputremapper/injection/event_reader.py | 2 +- inputremapper/injection/global_uinputs.py | 2 +- inputremapper/injection/injector.py | 2 +- inputremapper/injection/macros/macro.py | 2 +- inputremapper/injection/macros/parse.py | 2 +- .../injection/mapping_handlers/__init__.py | 2 +- .../mapping_handlers/abs_to_abs_handler.py | 2 +- .../mapping_handlers/abs_to_btn_handler.py | 2 +- .../mapping_handlers/abs_to_rel_handler.py | 2 +- .../mapping_handlers/axis_switch_handler.py | 2 +- .../mapping_handlers/axis_transform.py | 2 +- .../mapping_handlers/combination_handler.py | 2 +- .../mapping_handlers/hierarchy_handler.py | 2 +- .../injection/mapping_handlers/key_handler.py | 2 +- .../mapping_handlers/macro_handler.py | 2 +- .../mapping_handlers/mapping_handler.py | 2 +- .../mapping_handlers/mapping_parser.py | 2 +- .../mapping_handlers/null_handler.py | 2 +- .../mapping_handlers/rel_to_abs_handler.py | 2 +- .../mapping_handlers/rel_to_btn_handler.py | 2 +- .../mapping_handlers/rel_to_rel_handler.py | 2 +- inputremapper/injection/numlock.py | 2 +- inputremapper/input_event.py | 2 +- inputremapper/ipc/__init__.py | 2 +- inputremapper/ipc/pipe.py | 2 +- inputremapper/ipc/shared_dict.py | 2 +- inputremapper/ipc/socket.py | 2 +- inputremapper/logger/formatter.py | 2 +- inputremapper/logger/logger.py | 2 +- inputremapper/user.py | 2 +- inputremapper/utils.py | 2 +- setup.py | 2 +- tests/integration/__init__.py | 1 - tests/integration/test_components.py | 54 +++++++++---------- tests/integration/test_daemon.py | 10 ++-- tests/integration/test_data.py | 6 +-- tests/integration/test_gui.py | 19 ++++--- tests/integration/test_numlock.py | 6 +-- tests/integration/test_user_interface.py | 4 +- tests/lib/cleanup.py | 7 ++- tests/lib/constants.py | 2 +- tests/lib/fixture_pipes.py | 36 +++++++++++++ tests/lib/fixtures.py | 2 +- tests/lib/global_uinputs.py | 2 +- tests/lib/is_service_running.py | 32 +++++++++++ tests/lib/logger.py | 2 +- tests/lib/patches.py | 2 +- tests/lib/pipes.py | 2 +- tests/lib/project_root.py | 37 +++++++++++++ tests/lib/{stuff.py => spy.py} | 7 +-- tests/{test.py => lib/test_setup.py} | 44 ++------------- tests/lib/tmp.py | 2 +- tests/lib/xmodmap.py | 2 +- tests/unit/test_config.py | 6 +-- tests/unit/test_context.py | 6 +-- tests/unit/test_control.py | 6 +-- tests/unit/test_controller.py | 9 ++-- tests/unit/test_daemon.py | 6 +-- tests/unit/test_data_manager.py | 6 +-- .../test_axis_transformation.py | 6 +-- .../test_event_pipeline.py | 18 +++---- .../test_mapping_handlers.py | 26 ++++----- tests/unit/test_event_reader.py | 6 +-- tests/unit/test_global_uinputs.py | 10 ++-- tests/unit/test_groups.py | 6 +-- tests/unit/test_injector.py | 8 +-- tests/unit/test_input_config.py | 8 +-- tests/unit/test_input_event.py | 6 +-- tests/unit/test_ipc.py | 10 ++-- tests/unit/test_logger.py | 6 +-- tests/unit/test_macros.py | 13 +++-- tests/unit/test_mapping.py | 8 +-- tests/unit/test_message_broker.py | 8 +-- tests/unit/test_migrations.py | 4 +- tests/unit/test_paths.py | 6 +-- tests/unit/test_preset.py | 6 +-- tests/unit/test_reader.py | 10 ++-- tests/unit/test_system_mapping.py | 6 +-- tests/unit/test_test.py | 6 +-- tests/unit/test_user.py | 6 +-- tests/unit/test_util.py | 6 +-- 114 files changed, 349 insertions(+), 288 deletions(-) create mode 100644 tests/lib/fixture_pipes.py create mode 100644 tests/lib/is_service_running.py create mode 100644 tests/lib/project_root.py rename tests/lib/{stuff.py => spy.py} (89%) rename tests/{test.py => lib/test_setup.py} (70%) diff --git a/bin/input-remapper-control b/bin/input-remapper-control index d77b448cf..498e0d97d 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index 95e9fa38b..6b10859b7 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/bin/input-remapper-reader-service b/bin/input-remapper-reader-service index 0a5473c5a..be5838c65 100755 --- a/bin/input-remapper-reader-service +++ b/bin/input-remapper-reader-service @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/bin/input-remapper-service b/bin/input-remapper-service index bfaf9799e..180453dff 100755 --- a/bin/input-remapper-service +++ b/bin/input-remapper-service @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/base_config.py b/inputremapper/configs/base_config.py index 0bfb8263f..6b10726e6 100644 --- a/inputremapper/configs/base_config.py +++ b/inputremapper/configs/base_config.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/data.py b/inputremapper/configs/data.py index 65f2d0725..93a4652d9 100644 --- a/inputremapper/configs/data.py +++ b/inputremapper/configs/data.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/global_config.py b/inputremapper/configs/global_config.py index 6c94ef2c6..cb3579453 100644 --- a/inputremapper/configs/global_config.py +++ b/inputremapper/configs/global_config.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/input_config.py b/inputremapper/configs/input_config.py index b60554f70..009bf9a2f 100644 --- a/inputremapper/configs/input_config.py +++ b/inputremapper/configs/input_config.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/mapping.py b/inputremapper/configs/mapping.py index f019fcf37..c1e09b33b 100644 --- a/inputremapper/configs/mapping.py +++ b/inputremapper/configs/mapping.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index bb1e5a638..23cf31a7d 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/paths.py b/inputremapper/configs/paths.py index 6e62a7f53..b9183d6b6 100644 --- a/inputremapper/configs/paths.py +++ b/inputremapper/configs/paths.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/preset.py b/inputremapper/configs/preset.py index af80c636c..f637b236d 100644 --- a/inputremapper/configs/preset.py +++ b/inputremapper/configs/preset.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/system_mapping.py b/inputremapper/configs/system_mapping.py index 2a3677e09..86d710d7f 100644 --- a/inputremapper/configs/system_mapping.py +++ b/inputremapper/configs/system_mapping.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/configs/validation_errors.py b/inputremapper/configs/validation_errors.py index e4d7ce94b..156a7c980 100644 --- a/inputremapper/configs/validation_errors.py +++ b/inputremapper/configs/validation_errors.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index b738984ba..75c04f5a1 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/exceptions.py b/inputremapper/exceptions.py index 9b9bf6a0b..979a6f03a 100644 --- a/inputremapper/exceptions.py +++ b/inputremapper/exceptions.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/groups.py b/inputremapper/groups.py index 76ead984f..9a1865353 100644 --- a/inputremapper/groups.py +++ b/inputremapper/groups.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/autocompletion.py b/inputremapper/gui/autocompletion.py index 9ff7dd96f..c3c1df99f 100644 --- a/inputremapper/gui/autocompletion.py +++ b/inputremapper/gui/autocompletion.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/components/common.py b/inputremapper/gui/components/common.py index 275ba1d9f..27de5aea8 100644 --- a/inputremapper/gui/components/common.py +++ b/inputremapper/gui/components/common.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/components/device_groups.py b/inputremapper/gui/components/device_groups.py index ae73b161d..274f9fb2d 100644 --- a/inputremapper/gui/components/device_groups.py +++ b/inputremapper/gui/components/device_groups.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/components/editor.py b/inputremapper/gui/components/editor.py index 332eab84f..f250a42f4 100644 --- a/inputremapper/gui/components/editor.py +++ b/inputremapper/gui/components/editor.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/components/main.py b/inputremapper/gui/components/main.py index 6d21d8a0b..5169e41e5 100644 --- a/inputremapper/gui/components/main.py +++ b/inputremapper/gui/components/main.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/components/presets.py b/inputremapper/gui/components/presets.py index 46c0644d7..a8dc05b32 100644 --- a/inputremapper/gui/components/presets.py +++ b/inputremapper/gui/components/presets.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/controller.py b/inputremapper/gui/controller.py index e03c782d9..20c68cafa 100644 --- a/inputremapper/gui/controller.py +++ b/inputremapper/gui/controller.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/data_manager.py b/inputremapper/gui/data_manager.py index 5606ecb1b..2a6ab09d2 100644 --- a/inputremapper/gui/data_manager.py +++ b/inputremapper/gui/data_manager.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/gettext.py b/inputremapper/gui/gettext.py index 1d1f1687d..0788f8cce 100644 --- a/inputremapper/gui/gettext.py +++ b/inputremapper/gui/gettext.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/messages/message_broker.py b/inputremapper/gui/messages/message_broker.py index 0c90f241b..9c5d08fb2 100644 --- a/inputremapper/gui/messages/message_broker.py +++ b/inputremapper/gui/messages/message_broker.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/messages/message_data.py b/inputremapper/gui/messages/message_data.py index cf33b8882..3957f6e70 100644 --- a/inputremapper/gui/messages/message_data.py +++ b/inputremapper/gui/messages/message_data.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/messages/message_types.py b/inputremapper/gui/messages/message_types.py index df0ac2575..91b907c1a 100644 --- a/inputremapper/gui/messages/message_types.py +++ b/inputremapper/gui/messages/message_types.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/reader_client.py b/inputremapper/gui/reader_client.py index d392232ac..8e4e880f1 100644 --- a/inputremapper/gui/reader_client.py +++ b/inputremapper/gui/reader_client.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/user_interface.py b/inputremapper/gui/user_interface.py index 83ff636a6..ab8cab489 100644 --- a/inputremapper/gui/user_interface.py +++ b/inputremapper/gui/user_interface.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/gui/utils.py b/inputremapper/gui/utils.py index 0af377e38..d2d7530dd 100644 --- a/inputremapper/gui/utils.py +++ b/inputremapper/gui/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/context.py b/inputremapper/injection/context.py index 5e4c196b8..16e7afa12 100644 --- a/inputremapper/injection/context.py +++ b/inputremapper/injection/context.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/event_reader.py b/inputremapper/injection/event_reader.py index d125afc01..296793272 100644 --- a/inputremapper/injection/event_reader.py +++ b/inputremapper/injection/event_reader.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/global_uinputs.py b/inputremapper/injection/global_uinputs.py index 931b8e505..6c92008c8 100644 --- a/inputremapper/injection/global_uinputs.py +++ b/inputremapper/injection/global_uinputs.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/injector.py b/inputremapper/injection/injector.py index 15435c89a..c49a0013c 100644 --- a/inputremapper/injection/injector.py +++ b/inputremapper/injection/injector.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index 4fc389749..59bd36aca 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/macros/parse.py b/inputremapper/injection/macros/parse.py index acecf30f6..646d094f5 100644 --- a/inputremapper/injection/macros/parse.py +++ b/inputremapper/injection/macros/parse.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/__init__.py b/inputremapper/injection/mapping_handlers/__init__.py index 7c07f39cf..00ef810c0 100644 --- a/inputremapper/injection/mapping_handlers/__init__.py +++ b/inputremapper/injection/mapping_handlers/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py index 576c80b36..aa57e1523 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py index 123cd22bf..544786fde 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py index a4a6b863f..a73f8f1d9 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/axis_switch_handler.py b/inputremapper/injection/mapping_handlers/axis_switch_handler.py index 68a53f117..ae268f6d4 100644 --- a/inputremapper/injection/mapping_handlers/axis_switch_handler.py +++ b/inputremapper/injection/mapping_handlers/axis_switch_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/axis_transform.py b/inputremapper/injection/mapping_handlers/axis_transform.py index a81be65c2..ed8e46b4e 100644 --- a/inputremapper/injection/mapping_handlers/axis_transform.py +++ b/inputremapper/injection/mapping_handlers/axis_transform.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/combination_handler.py b/inputremapper/injection/mapping_handlers/combination_handler.py index 0a3d2018b..50a971a2f 100644 --- a/inputremapper/injection/mapping_handlers/combination_handler.py +++ b/inputremapper/injection/mapping_handlers/combination_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/hierarchy_handler.py b/inputremapper/injection/mapping_handlers/hierarchy_handler.py index cf611cca8..4f71581eb 100644 --- a/inputremapper/injection/mapping_handlers/hierarchy_handler.py +++ b/inputremapper/injection/mapping_handlers/hierarchy_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/key_handler.py b/inputremapper/injection/mapping_handlers/key_handler.py index f1ae0d62d..26ad0adb2 100644 --- a/inputremapper/injection/mapping_handlers/key_handler.py +++ b/inputremapper/injection/mapping_handlers/key_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/macro_handler.py b/inputremapper/injection/mapping_handlers/macro_handler.py index ea4c9f0c1..5bd5c1457 100644 --- a/inputremapper/injection/mapping_handlers/macro_handler.py +++ b/inputremapper/injection/mapping_handlers/macro_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/mapping_handler.py b/inputremapper/injection/mapping_handlers/mapping_handler.py index dc3a7a333..74b1b6da2 100644 --- a/inputremapper/injection/mapping_handlers/mapping_handler.py +++ b/inputremapper/injection/mapping_handlers/mapping_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/mapping_parser.py b/inputremapper/injection/mapping_handlers/mapping_parser.py index 333021acb..d42a816e4 100644 --- a/inputremapper/injection/mapping_handlers/mapping_parser.py +++ b/inputremapper/injection/mapping_handlers/mapping_parser.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/null_handler.py b/inputremapper/injection/mapping_handlers/null_handler.py index f8402c11f..ebd1d3720 100644 --- a/inputremapper/injection/mapping_handlers/null_handler.py +++ b/inputremapper/injection/mapping_handlers/null_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py index 10b5e11cb..d4abc30a8 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py index 42aed1257..f8e8b36c5 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py index 22ea6f1f3..e710bbdd2 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/injection/numlock.py b/inputremapper/injection/numlock.py index 8588a6773..af9e4a513 100644 --- a/inputremapper/injection/numlock.py +++ b/inputremapper/injection/numlock.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/input_event.py b/inputremapper/input_event.py index a3f013302..9a0fddf92 100644 --- a/inputremapper/input_event.py +++ b/inputremapper/input_event.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/ipc/__init__.py b/inputremapper/ipc/__init__.py index dd31976ae..d17180c85 100644 --- a/inputremapper/ipc/__init__.py +++ b/inputremapper/ipc/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/ipc/pipe.py b/inputremapper/ipc/pipe.py index a56ef5516..df4dbab95 100644 --- a/inputremapper/ipc/pipe.py +++ b/inputremapper/ipc/pipe.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/ipc/shared_dict.py b/inputremapper/ipc/shared_dict.py index 66b030f89..4bbb9213c 100644 --- a/inputremapper/ipc/shared_dict.py +++ b/inputremapper/ipc/shared_dict.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/ipc/socket.py b/inputremapper/ipc/socket.py index f6973c0e4..c4472c1f4 100644 --- a/inputremapper/ipc/socket.py +++ b/inputremapper/ipc/socket.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/logger/formatter.py b/inputremapper/logger/formatter.py index 67f0eb31a..74b4d1e4b 100644 --- a/inputremapper/logger/formatter.py +++ b/inputremapper/logger/formatter.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/logger/logger.py b/inputremapper/logger/logger.py index c6f76980d..1300a8c82 100644 --- a/inputremapper/logger/logger.py +++ b/inputremapper/logger/logger.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/user.py b/inputremapper/user.py index d9094a7b3..6a47d84e7 100644 --- a/inputremapper/user.py +++ b/inputremapper/user.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/inputremapper/utils.py b/inputremapper/utils.py index 68d42922d..feeee5994 100644 --- a/inputremapper/utils.py +++ b/inputremapper/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/setup.py b/setup.py index 5b768ae8b..94820acd0 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 66d541b11..a0922b87f 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1,6 +1,5 @@ """Tests that require a linux desktop environment to be running.""" -import tests.test import gi gi.require_version("Gdk", "3.0") diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index cfc434383..bcca189e3 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -34,7 +34,7 @@ from gi.repository import Gtk, GLib, GtkSource, Gdk from tests.lib.cleanup import quick_cleanup -from tests.lib.stuff import spy +from tests.lib.spy import spy from tests.lib.logger import logger from inputremapper.gui.controller import Controller @@ -85,7 +85,7 @@ ) from inputremapper.configs.mapping import MappingData from inputremapper.configs.input_config import InputCombination, InputConfig -from tests.test import setup_tests +from tests.lib.test_setup import test_setup class ComponentBaseTest(unittest.TestCase): @@ -169,7 +169,7 @@ def get_child_icons(flow_box: Gtk.FlowBox): return icon_names -@setup_tests +@test_setup class TestDeviceGroupSelection(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -234,7 +234,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.load_group.assert_not_called() -@setup_tests +@test_setup class TestTargetSelection(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -282,7 +282,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.update_mapping.assert_not_called() -@setup_tests +@test_setup class TestPresetSelection(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -352,7 +352,7 @@ def test_loads_preset(self): self.controller_mock.load_preset.assert_called_once_with("preset2") -@setup_tests +@test_setup class TestMappingListbox(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -495,7 +495,7 @@ def test_sorts_empty_mapping_to_bottom(self): self.assertEqual(bottom_row.combination, InputCombination.empty_combination()) -@setup_tests +@test_setup class TestMappingSelectionLabel(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -780,7 +780,7 @@ def test_removes_name_when_name_matches_combination(self): self.controller_mock.update_mapping.assert_called_once_with(name="") -@setup_tests +@test_setup class TestGdkEventRecorder(ComponentBaseTest): def _emit_key(self, window, code, type_): event = Gdk.Event() @@ -831,7 +831,7 @@ def test_records_combinations(self): self.assertEqual(label.get_text(), "a") -@setup_tests +@test_setup class TestCodeEditor(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -935,7 +935,7 @@ def unfocus(): self.assertNotIn("opaque-text", self.gui.get_style_context().list_classes()) -@setup_tests +@test_setup class TestRecordingToggle(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -982,7 +982,7 @@ def test_shows_not_recording_when_recording_finished(self): self.assert_not_recording() -@setup_tests +@test_setup class TestStatusBar(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1066,7 +1066,7 @@ def test_sets_msg_as_tooltip_if_tooltip_is_none(self): self.assertEqual(self.get_tooltip(), "msg") -@setup_tests +@test_setup class TestAutoloadSwitch(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1094,7 +1094,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.set_autoload.assert_not_called() -@setup_tests +@test_setup class TestReleaseCombinationSwitch(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1126,7 +1126,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.update_mapping.assert_not_called() -@setup_tests +@test_setup class TestEventEntry(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1147,7 +1147,7 @@ def test_move_event(self): ) -@setup_tests +@test_setup class TestCombinationListbox(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1210,7 +1210,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.load_event.assert_not_called() -@setup_tests +@test_setup class TestAnalogInputSwitch(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1254,7 +1254,7 @@ def test_enables_switch_when_axis_event(self): self.assertTrue(self.gui.get_sensitive()) -@setup_tests +@test_setup class TestTriggerThresholdInput(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1300,7 +1300,7 @@ def test_updates_configuration_according_to_selected_event(self): self.assert_key_event_config() -@setup_tests +@test_setup class TestReleaseTimeoutInput(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1397,7 +1397,7 @@ def test_disables_input_based_on_input_combination(self): self.assertLess(self.gui.get_opacity(), 0.6) -@setup_tests +@test_setup class TestOutputAxisSelector(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1466,7 +1466,7 @@ def test_updates_dropdown_model(self): self.assertEqual(len(self.gui.get_model()), 9) -@setup_tests +@test_setup class TestKeyAxisStackSwitcher(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1529,7 +1529,7 @@ def test_avoids_infinite_recursion(self): self.controller_mock.update_mapping.assert_not_called() -@setup_tests +@test_setup class TestTransformationDrawArea(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1567,7 +1567,7 @@ def test_redraws_when_mapping_updates(self): mock.assert_called() -@setup_tests +@test_setup class TestSliders(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1644,7 +1644,7 @@ def test_avoids_recursion(self): self.controller_mock.update_mapping.assert_not_called() -@setup_tests +@test_setup class TestRelativeInputCutoffInput(ComponentBaseTest): def setUp(self) -> None: super().setUp() @@ -1746,7 +1746,7 @@ def test_enables_input(self): self.assert_active() -@setup_tests +@test_setup class TestRequireActiveMapping(ComponentBaseTest): def test_no_reqorded_input_required(self): self.box = Gtk.Box() @@ -1810,7 +1810,7 @@ def assert_active(self, widget: Gtk.Widget): self.assertEqual(widget.get_opacity(), 1) -@setup_tests +@test_setup class TestStack(ComponentBaseTest): def test_switches_pages(self): self.stack = Gtk.Stack() @@ -1830,7 +1830,7 @@ def test_switches_pages(self): self.assertEqual(self.stack.get_visible_child_name(), "Editor") -@setup_tests +@test_setup class TestBreadcrumbs(ComponentBaseTest): def test_breadcrumbs(self): self.label_1 = Gtk.Label() diff --git a/tests/integration/test_daemon.py b/tests/integration/test_daemon.py index fd478fb22..796840367 100644 --- a/tests/integration/test_daemon.py +++ b/tests/integration/test_daemon.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -17,7 +17,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from tests.test import is_service_running, setup_tests +from tests.lib.test_setup import is_service_running import os import multiprocessing @@ -30,7 +30,7 @@ from gi.repository import Gtk from inputremapper.daemon import Daemon, BUS_NAME -from tests.test import setup_tests +from tests.lib.test_setup import test_setup def gtk_iteration(): @@ -39,8 +39,8 @@ def gtk_iteration(): Gtk.main_iteration() -@setup_tests -@setup_tests +@test_setup +@test_setup class TestDBusDaemon(unittest.TestCase): def setUp(self): self.process = multiprocessing.Process( diff --git a/tests/integration/test_data.py b/tests/integration/test_data.py index 949c504ae..ee2ccde94 100644 --- a/tests/integration/test_data.py +++ b/tests/integration/test_data.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -24,10 +24,10 @@ import pkg_resources from inputremapper.configs.data import get_data_path -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestData(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index d08464e17..dbafe30ca 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -55,9 +55,8 @@ from tests.lib.global_uinputs import reset_global_uinputs_for_service from tests.lib.logger import logger from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe -from tests.lib.stuff import spy - -from tests.test import get_project_root +from tests.lib.project_root import get_project_root +from tests.lib.spy import spy gi.require_version("Gdk", "3.0") gi.require_version("Gtk", "3.0") @@ -89,7 +88,7 @@ from inputremapper.injection.injector import InjectorState from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.daemon import Daemon, DaemonProxy -from tests.test import setup_tests +from tests.lib.test_setup import test_setup # iterate a few times when Gtk.main() is called, but don't block @@ -187,7 +186,7 @@ def get_keyval(self): return True, self.keyval -@setup_tests +@test_setup class TestGroupsFromReaderService(unittest.TestCase): def setUp(self): # don't try to connect, return an object instance of it instead @@ -465,7 +464,7 @@ def sleep(self, num_events): gtk_iteration() -@setup_tests +@test_setup class TestColors(GuiTestBase): # requires a running ui, otherwise fails with segmentation faults def test_get_color_falls_back(self): @@ -515,7 +514,7 @@ def test_get_colors(self): self._test_color_wont_fallback(Colors.get_font_color, Colors.fallback_font) -@setup_tests +@test_setup class TestGui(GuiTestBase): """For tests that use the window. @@ -2041,7 +2040,7 @@ def test_enable_disable_output(self): self.assertFalse(self.output_box.get_sensitive()) -@setup_tests +@test_setup class TestAutocompletion(GuiTestBase): def press_key(self, keyval): event = Gdk.EventKey() @@ -2255,7 +2254,7 @@ def test_cycling(self): ) -@setup_tests +@test_setup class TestDebounce(unittest.TestCase): def test_debounce(self): calls = 0 diff --git a/tests/integration/test_numlock.py b/tests/integration/test_numlock.py index b65bbdb27..e47832a80 100644 --- a/tests/integration/test_numlock.py +++ b/tests/integration/test_numlock.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -24,10 +24,10 @@ import unittest from inputremapper.injection.numlock import is_numlock_on, set_numlock, ensure_numlock -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestNumlock(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/tests/integration/test_user_interface.py b/tests/integration/test_user_interface.py index 2b7862637..9ab6ae193 100644 --- a/tests/integration/test_user_interface.py +++ b/tests/integration/test_user_interface.py @@ -16,10 +16,10 @@ from inputremapper.gui.user_interface import UserInterface from inputremapper.configs.mapping import MappingData from inputremapper.configs.input_config import InputCombination, InputConfig -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestUserInterface(unittest.TestCase): def setUp(self) -> None: self.message_broker = MessageBroker() diff --git a/tests/lib/cleanup.py b/tests/lib/cleanup.py index 434094fc9..14b688a70 100644 --- a/tests/lib/cleanup.py +++ b/tests/lib/cleanup.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -20,6 +20,7 @@ from __future__ import annotations +import copy import os import shutil import time @@ -42,10 +43,12 @@ from tests.lib.constants import EVENT_READ_TIMEOUT from tests.lib.tmp import tmp from tests.lib.fixtures import fixtures -from tests.lib.stuff import environ_copy from tests.lib.patches import uinputs +environ_copy = copy.deepcopy(os.environ) + + def join_children(): """Wait for child processes to exit. Stop them if it takes too long.""" this = psutil.Process(os.getpid()) diff --git a/tests/lib/constants.py b/tests/lib/constants.py index 7a6638b0e..783c92cdc 100644 --- a/tests/lib/constants.py +++ b/tests/lib/constants.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/tests/lib/fixture_pipes.py b/tests/lib/fixture_pipes.py new file mode 100644 index 000000000..f12d31584 --- /dev/null +++ b/tests/lib/fixture_pipes.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2024 sezanzeb +# +# This file is part of input-remapper. +# +# input-remapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# input-remapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with input-remapper. If not, see . + +from __future__ import annotations + +from tests.lib.fixtures import fixtures +from tests.lib.pipes import setup_pipe, close_pipe + + +def create_fixture_pipes(): + # make sure those pipes exist before any process (the reader-service) gets forked, + # so that events can be pushed after the fork. + for _fixture in fixtures: + setup_pipe(_fixture) + + +def remove_fixture_pipes(): + for _fixture in fixtures: + close_pipe(_fixture) diff --git a/tests/lib/fixtures.py b/tests/lib/fixtures.py index 6d46088de..2cbe9b6c2 100644 --- a/tests/lib/fixtures.py +++ b/tests/lib/fixtures.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/tests/lib/global_uinputs.py b/tests/lib/global_uinputs.py index d3b76a23b..cf6608c3c 100644 --- a/tests/lib/global_uinputs.py +++ b/tests/lib/global_uinputs.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/tests/lib/is_service_running.py b/tests/lib/is_service_running.py new file mode 100644 index 000000000..86e789511 --- /dev/null +++ b/tests/lib/is_service_running.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2024 sezanzeb +# +# This file is part of input-remapper. +# +# input-remapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# input-remapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with input-remapper. If not, see . + +from __future__ import annotations + +import subprocess + + +def is_service_running(): + """Check if the daemon is running.""" + try: + subprocess.check_output(["pgrep", "-f", "input-remapper-service"]) + return True + except subprocess.CalledProcessError: + return False diff --git a/tests/lib/logger.py b/tests/lib/logger.py index 91518f8de..9a792acdb 100644 --- a/tests/lib/logger.py +++ b/tests/lib/logger.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/tests/lib/patches.py b/tests/lib/patches.py index 7be860161..fde7c7cef 100644 --- a/tests/lib/patches.py +++ b/tests/lib/patches.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/tests/lib/pipes.py b/tests/lib/pipes.py index 42780613b..dc0d05cb6 100644 --- a/tests/lib/pipes.py +++ b/tests/lib/pipes.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/tests/lib/project_root.py b/tests/lib/project_root.py new file mode 100644 index 000000000..53ab9e594 --- /dev/null +++ b/tests/lib/project_root.py @@ -0,0 +1,37 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2024 sezanzeb +# +# This file is part of input-remapper. +# +# input-remapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# input-remapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with input-remapper. If not, see . + +from __future__ import annotations + +import os + + +def get_project_root(): + """Find the projects root, i.e. the uppermost directory of the repo.""" + # when tests are started in pycharm via the green arrow, the working directory + # is not the project root. Go up until it is found. + root = os.getcwd() + for _ in range(10): + if "setup.py" in os.listdir(root): + return root + + root = os.path.dirname(root) + + raise Exception("Could not find project root") diff --git a/tests/lib/stuff.py b/tests/lib/spy.py similarity index 89% rename from tests/lib/stuff.py rename to tests/lib/spy.py index 1bf9913a8..5475e1f1a 100644 --- a/tests/lib/stuff.py +++ b/tests/lib/spy.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -18,14 +18,9 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -import os -import copy from unittest.mock import patch def spy(obj, name): """Convenient wrapper for patch.object(..., ..., wraps=...).""" return patch.object(obj, name, wraps=obj.__getattribute__(name)) - - -environ_copy = copy.deepcopy(os.environ) diff --git a/tests/test.py b/tests/lib/test_setup.py similarity index 70% rename from tests/test.py rename to tests/lib/test_setup.py index 621a5eeb9..222030586 100644 --- a/tests/test.py +++ b/tests/lib/test_setup.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -21,52 +21,16 @@ from __future__ import annotations import os -import subprocess import tracemalloc from tests.lib.cleanup import cleanup, quick_cleanup -from tests.lib.fixtures import fixtures +from tests.lib.fixture_pipes import create_fixture_pipes, remove_fixture_pipes +from tests.lib.is_service_running import is_service_running from tests.lib.logger import update_inputremapper_verbosity from tests.lib.patches import create_patches -from tests.lib.pipes import setup_pipe, close_pipe -def get_project_root(): - """Find the projects root, i.e. the uppermost directory of the repo.""" - # when tests are started in pycharm via the green arrow, the working directory - # is not the project root. Go up until it is found. - root = os.getcwd() - for _ in range(10): - if "setup.py" in os.listdir(root): - return root - - root = os.path.dirname(root) - - raise Exception("Could not find project root") - - -def is_service_running(): - """Check if the daemon is running.""" - try: - subprocess.check_output(["pgrep", "-f", "input-remapper-service"]) - return True - except subprocess.CalledProcessError: - return False - - -def create_fixture_pipes(): - # make sure those pipes exist before any process (the reader-service) gets forked, - # so that events can be pushed after the fork. - for _fixture in fixtures: - setup_pipe(_fixture) - - -def remove_fixture_pipes(): - for _fixture in fixtures: - close_pipe(_fixture) - - -def setup_tests(cls): +def test_setup(cls): """A class decorator to - apply the patches to all tests - check if the deamon is already running diff --git a/tests/lib/tmp.py b/tests/lib/tmp.py index 863b431cc..b9f832f44 100644 --- a/tests/lib/tmp.py +++ b/tests/lib/tmp.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/tests/lib/xmodmap.py b/tests/lib/xmodmap.py index 141dbe404..53947636c 100644 --- a/tests/lib/xmodmap.py +++ b/tests/lib/xmodmap.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index e1292f713..13ff5261d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -27,10 +27,10 @@ from tests.lib.cleanup import quick_cleanup from tests.lib.tmp import tmp -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestConfig(unittest.TestCase): def tearDown(self): quick_cleanup() diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index df0d14de6..ffd0f0446 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -33,10 +33,10 @@ from inputremapper.configs.preset import Preset from inputremapper.configs.mapping import Mapping from inputremapper.configs.input_config import InputCombination -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestContext(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index 6fa303dc5..775ab91e1 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -37,7 +37,7 @@ from inputremapper.configs.preset import Preset from inputremapper.configs.paths import PathUtils from inputremapper.groups import groups -from tests.test import setup_tests +from tests.lib.test_setup import test_setup def import_control(): @@ -65,7 +65,7 @@ def import_control(): ) -@setup_tests +@test_setup class TestControl(unittest.TestCase): def setUp(self): self.input_remapper_control = InputRemapperControl() diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index 1d6f91f10..b570ecd9e 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -28,7 +28,6 @@ from inputremapper.configs.system_mapping import system_mapping from inputremapper.injection.injector import InjectorState -from tests.lib.logger import logger gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -54,7 +53,7 @@ from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.configs.mapping import UIMapping, MappingData, Mapping from tests.lib.cleanup import quick_cleanup -from tests.lib.stuff import spy +from tests.lib.spy import spy from tests.lib.patches import FakeDaemonProxy from tests.lib.fixtures import fixtures, prepare_presets from inputremapper.configs.global_config import GlobalConfig @@ -62,10 +61,10 @@ from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestController(unittest.TestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index 7ed1aea70..6aec4d154 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -47,14 +47,14 @@ from inputremapper.injection.injector import InjectorState from inputremapper.daemon import Daemon from inputremapper.injection.global_uinputs import global_uinputs -from tests.test import setup_tests, is_service_running +from tests.lib.test_setup import test_setup, is_service_running check_output = subprocess.check_output os_system = os.system dbus_get = type(SystemBus()).get -@setup_tests +@test_setup class TestDaemon(unittest.TestCase): new_fixture_path = "/dev/input/event9876" diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index f3433a0b5..b285b8f64 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -47,7 +47,7 @@ from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME -from tests.test import setup_tests +from tests.lib.test_setup import test_setup class Listener: @@ -58,7 +58,7 @@ def __call__(self, data): self.calls.append(data) -@setup_tests +@test_setup class TestDataManager(unittest.TestCase): def setUp(self) -> None: self.message_broker = MessageBroker() diff --git a/tests/unit/test_event_pipeline/test_axis_transformation.py b/tests/unit/test_event_pipeline/test_axis_transformation.py index 93ad09253..180174369 100644 --- a/tests/unit/test_event_pipeline/test_axis_transformation.py +++ b/tests/unit/test_event_pipeline/test_axis_transformation.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -24,10 +24,10 @@ from typing import Iterable, List from inputremapper.injection.mapping_handlers.axis_transform import Transformation -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestAxisTransformation(unittest.TestCase): @dataclasses.dataclass class InitArgs: diff --git a/tests/unit/test_event_pipeline/test_event_pipeline.py b/tests/unit/test_event_pipeline/test_event_pipeline.py index 9015c8a79..db7583793 100644 --- a/tests/unit/test_event_pipeline/test_event_pipeline.py +++ b/tests/unit/test_event_pipeline/test_event_pipeline.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -63,7 +63,7 @@ from tests.lib.logger import logger from tests.lib.constants import MAX_ABS, MIN_ABS from tests.lib.fixtures import Fixture, fixtures -from tests.test import setup_tests +from tests.lib.test_setup import test_setup class EventPipelineTestBase(unittest.IsolatedAsyncioTestCase): @@ -109,7 +109,7 @@ def create_event_reader( return reader -@setup_tests +@test_setup class TestIdk(EventPipelineTestBase): async def test_any_event_as_button(self): """As long as there is an event handler and a mapping we should be able @@ -935,7 +935,7 @@ async def test_key_axis_combination_to_disable(self): ) -@setup_tests +@test_setup class TestAbsToAbs(EventPipelineTestBase): async def test_abs_to_abs(self): gain = 0.5 @@ -1036,7 +1036,7 @@ async def test_abs_to_abs_with_input_switch(self): ) -@setup_tests +@test_setup class TestRelToAbs(EventPipelineTestBase): async def test_rel_to_abs(self): timestamp = 0 @@ -1172,7 +1172,7 @@ async def test_rel_to_abs_with_input_switch(self): ) -@setup_tests +@test_setup class TestAbsToRel(EventPipelineTestBase): async def test_abs_to_rel(self): """Map gamepad EV_ABS events to EV_REL events.""" @@ -1319,7 +1319,7 @@ async def test_abs_to_wheel_hi_res_quirk(self): self.assertAlmostEqual(rel_hwheel, rel_hwheel_hi_res / 120, places=0) -@setup_tests +@test_setup class TestRelToBtn(EventPipelineTestBase): async def test_rel_to_btn(self): """Rel axis mapped to buttons are automatically released if no new rel event arrives.""" @@ -1452,7 +1452,7 @@ async def test_rel_trigger_threshold(self): ) -@setup_tests +@test_setup class TestAbsToBtn(EventPipelineTestBase): async def test_abs_trigger_threshold(self): """Test that different activation points for abs_to_btn work correctly.""" @@ -1521,7 +1521,7 @@ async def test_abs_trigger_threshold(self): self.assertEqual(len(forwarded_history), 0) -@setup_tests +@test_setup class TestRelToRel(EventPipelineTestBase): async def _test(self, input_code, input_value, output_code, output_value, gain=1): preset = Preset() diff --git a/tests/unit/test_event_pipeline/test_mapping_handlers.py b/tests/unit/test_event_pipeline/test_mapping_handlers.py index 7b799c77d..1b3262740 100644 --- a/tests/unit/test_event_pipeline/test_mapping_handlers.py +++ b/tests/unit/test_event_pipeline/test_mapping_handlers.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -67,7 +67,7 @@ from tests.lib.patches import InputDevice from tests.lib.constants import MAX_ABS from tests.lib.fixtures import fixtures -from tests.test import setup_tests +from tests.lib.test_setup import test_setup class BaseTests: @@ -90,7 +90,7 @@ def test_reset(self): mock.reset.assert_called() -@setup_tests +@test_setup class TestAxisSwitchHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -111,7 +111,7 @@ def setUp(self): ) -@setup_tests +@test_setup class TestAbsToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -127,7 +127,7 @@ def setUp(self): ) -@setup_tests +@test_setup class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) @@ -154,7 +154,7 @@ async def test_reset(self): ) -@setup_tests +@test_setup class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)]) @@ -218,7 +218,7 @@ async def test_rate_stays(self): self.assertEqual(self.handler._observed_rate, DEFAULT_REL_RATE) -@setup_tests +@test_setup class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) @@ -247,7 +247,7 @@ async def test_reset(self): self.assertEqual(count, global_uinputs.get_uinput("mouse").write_count) -@setup_tests +@test_setup class TestCombinationHandler(BaseTests, unittest.IsolatedAsyncioTestCase): handler: CombinationHandler @@ -388,7 +388,7 @@ def test_no_forwards(self): self.assertListEqual(uinputs[self.keyboard_hash].write_history, []) -@setup_tests +@test_setup class TestHierarchyHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): self.mock1 = MagicMock() @@ -406,7 +406,7 @@ def test_reset(self): self.mock3.reset.assert_called() -@setup_tests +@test_setup class TestKeyHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -439,7 +439,7 @@ def test_reset(self): self.assertEqual(len(history), 2) -@setup_tests +@test_setup class TestMacroHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -479,7 +479,7 @@ async def test_reset(self): self.assertEqual(len(history), 4) -@setup_tests +@test_setup class TestRelToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination( @@ -495,7 +495,7 @@ def setUp(self): ) -@setup_tests +@test_setup class TestRelToRelHanlder(BaseTests, unittest.IsolatedAsyncioTestCase): handler: RelToRelHandler diff --git a/tests/unit/test_event_reader.py b/tests/unit/test_event_reader.py index a9223ef14..b59a65880 100644 --- a/tests/unit/test_event_reader.py +++ b/tests/unit/test_event_reader.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -47,10 +47,10 @@ from inputremapper.utils import get_device_hash from tests.lib.fixtures import fixtures from tests.lib.cleanup import quick_cleanup -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestEventReader(unittest.IsolatedAsyncioTestCase): def setUp(self): self.gamepad_source = evdev.InputDevice(fixtures.gamepad.path) diff --git a/tests/unit/test_global_uinputs.py b/tests/unit/test_global_uinputs.py index e0793b452..817f9fc9c 100644 --- a/tests/unit/test_global_uinputs.py +++ b/tests/unit/test_global_uinputs.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -26,8 +26,6 @@ from unittest.mock import patch from evdev.ecodes import ( - EV_KEY, - EV_ABS, KEY_A, ABS_X, ) @@ -38,10 +36,10 @@ GlobalUInputs, ) from inputremapper.exceptions import EventNotHandled, UinputNotAvailable -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestFrontendUinput(unittest.TestCase): def setUp(self) -> None: cleanup() @@ -59,7 +57,7 @@ def test_init(self): self.assertEqual(uinput_custom.capabilities(), capabilities) -@setup_tests +@test_setup class TestGlobalUinputs(unittest.TestCase): def setUp(self) -> None: cleanup() diff --git a/tests/unit/test_groups.py b/tests/unit/test_groups.py index 06e49d7cf..2329315ea 100644 --- a/tests/unit/test_groups.py +++ b/tests/unit/test_groups.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -37,7 +37,7 @@ DeviceType, _Group, ) -from tests.test import setup_tests +from tests.lib.test_setup import test_setup class FakePipe: @@ -47,7 +47,7 @@ def send(self, groups): self.groups = groups -@setup_tests +@test_setup class TestGroups(unittest.TestCase): def tearDown(self): quick_cleanup() diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index 87c3267c3..9c38afeca 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -67,7 +67,7 @@ from inputremapper.injection.macros.parse import parse from inputremapper.injection.context import Context from inputremapper.groups import groups, classify, DeviceType -from tests.test import setup_tests +from tests.lib.test_setup import test_setup def wait_for_uinput_write(): @@ -77,7 +77,7 @@ def wait_for_uinput_write(): return float(time.time() - start) -@setup_tests +@test_setup class TestInjector(unittest.IsolatedAsyncioTestCase): new_gamepad_path = "/dev/input/event100" @@ -546,7 +546,7 @@ def test_is_in_capabilities(self): self.assertTrue(is_in_capabilities(key, capabilities)) -@setup_tests +@test_setup class TestModifyCapabilities(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/tests/unit/test_input_config.py b/tests/unit/test_input_config.py index 97d62170d..34821e879 100644 --- a/tests/unit/test_input_config.py +++ b/tests/unit/test_input_config.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -44,10 +44,10 @@ ) from inputremapper.configs.input_config import InputCombination, InputConfig -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestInputConfig(unittest.TestCase): def test_input_config(self): test_cases = [ @@ -322,7 +322,7 @@ def test_is_immutable(self): input_config.origin_hash = "foo" -@setup_tests +@test_setup class TestInputCombination(unittest.TestCase): def test_eq(self): a = InputCombination( diff --git a/tests/unit/test_input_event.py b/tests/unit/test_input_event.py index 7f3621c3c..7a1531dbe 100644 --- a/tests/unit/test_input_event.py +++ b/tests/unit/test_input_event.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -23,10 +23,10 @@ import evdev from dataclasses import FrozenInstanceError from inputremapper.input_event import InputEvent -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestInputEvent(unittest.TestCase): def test_from_event(self): e1 = InputEvent.from_event(evdev.InputEvent(1, 2, 3, 4, 5)) diff --git a/tests/unit/test_ipc.py b/tests/unit/test_ipc.py index 8d9712269..28c7ddee9 100644 --- a/tests/unit/test_ipc.py +++ b/tests/unit/test_ipc.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -31,10 +31,10 @@ from inputremapper.ipc.pipe import Pipe from inputremapper.ipc.shared_dict import SharedDict from inputremapper.ipc.socket import Server, Client, Base -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestSharedDict(unittest.TestCase): def setUp(self): self.shared_dict = SharedDict() @@ -54,7 +54,7 @@ def test_set_get(self): self.assertEqual(self.shared_dict["a"], 3) -@setup_tests +@test_setup class TestSocket(unittest.TestCase): def test_socket(self): def test(s1, s2): @@ -130,7 +130,7 @@ def test_base_abstract(self): self.assertRaises(NotImplementedError, lambda: Base.fileno(None)) -@setup_tests +@test_setup class TestPipe(unittest.IsolatedAsyncioTestCase): def test_pipe_single(self): p1 = Pipe(os.path.join(tmp, "pipe")) diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index 5b024cdfe..dcaaad157 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -32,7 +32,7 @@ logger, ColorfulFormatter, ) -from tests.test import setup_tests +from tests.lib.test_setup import test_setup def add_filehandler(log_path: str, debug: bool) -> None: @@ -45,7 +45,7 @@ def add_filehandler(log_path: str, debug: bool) -> None: logger.info('Starting logging to "%s"', log_path) -@setup_tests +@test_setup class TestLogger(unittest.TestCase): def tearDown(self): logger.update_verbosity(debug=True) diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index 9a750da22..dfe79a6c0 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -28,7 +28,6 @@ from evdev.ecodes import ( EV_REL, - EV_ABS, EV_KEY, ABS_Y, REL_Y, @@ -71,7 +70,7 @@ from inputremapper.input_event import InputEvent from tests.lib.logger import logger from tests.lib.cleanup import quick_cleanup -from tests.test import setup_tests +from tests.lib.test_setup import test_setup class MacroTestBase(unittest.IsolatedAsyncioTestCase): @@ -124,7 +123,7 @@ class DummyMapping: target_uinput = "keyboard + mouse" -@setup_tests +@test_setup class TestMacros(MacroTestBase): async def test_named_parameter(self): result = [] @@ -1154,7 +1153,7 @@ async def test_multiline_macro_and_comments(self): ) -@setup_tests +@test_setup class TestIfEq(MacroTestBase): async def test_ifeq_runs(self): # deprecated ifeq function, but kept for compatibility reasons @@ -1307,7 +1306,7 @@ def set_foo(value): ) -@setup_tests +@test_setup class TestIfSingle(MacroTestBase): async def test_if_single(self): macro = parse("if_single(key(x), key(y))", self.context, DummyMapping) @@ -1455,7 +1454,7 @@ async def test_if_single_ignores_joystick(self): self.assertListEqual(self.result, [(EV_KEY, code_a, 1), (EV_KEY, code_a, 0)]) -@setup_tests +@test_setup class TestIfTap(MacroTestBase): async def test_if_tap(self): macro = parse("if_tap(key(x), key(y), 100)", self.context, DummyMapping) diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py index bcfa7ffdd..283747c10 100644 --- a/tests/unit/test_mapping.py +++ b/tests/unit/test_mapping.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -40,10 +40,10 @@ from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.gui.messages.message_broker import MessageType -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestMapping(unittest.IsolatedAsyncioTestCase): def test_init(self): """Test init and that defaults are set.""" @@ -412,7 +412,7 @@ def test_wrong_target_for_macro(self): ) -@setup_tests +@test_setup class TestUIMapping(unittest.IsolatedAsyncioTestCase): def test_init(self): """Should be able to initialize without throwing errors.""" diff --git a/tests/unit/test_message_broker.py b/tests/unit/test_message_broker.py index c58ebf4bf..6373c11a9 100644 --- a/tests/unit/test_message_broker.py +++ b/tests/unit/test_message_broker.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -22,7 +22,7 @@ from dataclasses import dataclass from inputremapper.gui.messages.message_broker import MessageBroker, MessageType, Signal -from tests.test import setup_tests +from tests.lib.test_setup import test_setup class Listener: @@ -39,7 +39,7 @@ class Message: msg: str -@setup_tests +@test_setup class TestMessageBroker(unittest.TestCase): def test_calls_listeners(self): """The correct Listeners get called""" @@ -101,7 +101,7 @@ def listener4(_): self.assertEqual([4, 4, 4], calls[3:]) -@setup_tests +@test_setup class TestSignal(unittest.TestCase): def test_eq(self): self.assertEqual(Signal(MessageType.uinputs), Signal(MessageType.uinputs)) diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index 59904f32e..cc3da078a 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -44,10 +44,10 @@ from inputremapper.configs.preset import Preset from inputremapper.logger.logger import VERSION from inputremapper.user import UserUtils -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestMigrations(unittest.TestCase): def setUp(self): # some extra care to ensure those tests are not destroying actual presets diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py index f4959685b..637f2c902 100644 --- a/tests/unit/test_paths.py +++ b/tests/unit/test_paths.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -27,14 +27,14 @@ import tempfile from inputremapper.configs.paths import PathUtils -from tests.test import setup_tests +from tests.lib.test_setup import test_setup def _raise(error): raise error -@setup_tests +@test_setup class TestPaths(unittest.TestCase): def tearDown(self): quick_cleanup() diff --git a/tests/unit/test_preset.py b/tests/unit/test_preset.py index 3a57cae99..2a72b699a 100644 --- a/tests/unit/test_preset.py +++ b/tests/unit/test_preset.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -30,10 +30,10 @@ from inputremapper.configs.preset import Preset from inputremapper.configs.input_config import InputCombination, InputConfig from tests.lib.cleanup import quick_cleanup -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestPreset(unittest.TestCase): def setUp(self): self.preset = Preset(PathUtils.get_preset_path("foo", "bar2")) diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index fe05cea04..38c4f6ce3 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -63,8 +63,8 @@ ) from tests.lib.pipes import push_event, push_events from tests.lib.fixtures import fixtures -from tests.lib.stuff import spy -from tests.test import setup_tests +from tests.lib.spy import spy +from tests.lib.test_setup import test_setup CODE_1 = 100 CODE_2 = 101 @@ -90,7 +90,7 @@ def wait(func, timeout=1.0): break -@setup_tests +@test_setup class TestReaderAsyncio(unittest.IsolatedAsyncioTestCase): def setUp(self): self.reader_service = None @@ -159,7 +159,7 @@ def remember_context(*args, **kwargs): self.assertEqual([call[0] for call in write_spy.call_args_list], events) -@setup_tests +@test_setup class TestReaderMultiprocessing(unittest.TestCase): def setUp(self): self.reader_service_process = None diff --git a/tests/unit/test_system_mapping.py b/tests/unit/test_system_mapping.py index b9d6ed809..678f72ba3 100644 --- a/tests/unit/test_system_mapping.py +++ b/tests/unit/test_system_mapping.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -29,10 +29,10 @@ from inputremapper.configs.paths import PathUtils from inputremapper.configs.system_mapping import SystemMapping, XMODMAP_FILENAME from tests.lib.cleanup import quick_cleanup -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestSystemMapping(unittest.TestCase): def tearDown(self): quick_cleanup() diff --git a/tests/unit/test_test.py b/tests/unit/test_test.py index 227f91b6e..0a553a47a 100644 --- a/tests/unit/test_test.py +++ b/tests/unit/test_test.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -40,10 +40,10 @@ from inputremapper.input_event import InputEvent from inputremapper.utils import get_device_hash from inputremapper.gui.messages.message_broker import MessageBroker -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestTest(unittest.TestCase): def test_stubs(self): self.assertIsNotNone(groups.find(key="Foo Device 2")) diff --git a/tests/unit/test_user.py b/tests/unit/test_user.py index dcd31aa55..591b14561 100644 --- a/tests/unit/test_user.py +++ b/tests/unit/test_user.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -26,14 +26,14 @@ from unittest import mock from inputremapper.user import UserUtils -from tests.test import setup_tests +from tests.lib.test_setup import test_setup def _raise(error): raise error -@setup_tests +@test_setup class TestUser(unittest.TestCase): def tearDown(self): quick_cleanup() diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 5c11ac44a..e8069de05 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2023 sezanzeb +# Copyright (C) 2024 sezanzeb # # This file is part of input-remapper. # @@ -24,10 +24,10 @@ from evdev._ecodes import EV_ABS, ABS_X, BTN_WEST, BTN_Y, EV_KEY, KEY_A from inputremapper.utils import get_evdev_constant_name -from tests.test import setup_tests +from tests.lib.test_setup import test_setup -@setup_tests +@test_setup class TestUtil(unittest.TestCase): def test_get_evdev_constant_name(self): # BTN_WEST and BTN_Y both are code 308. I don't care which one is chosen From a055911656996952ebe9921e8e8216c201c5ce6b Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:27:51 +0200 Subject: [PATCH 12/32] unittest discover in github pipeline --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d43023e35..ccc888de0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,4 +34,4 @@ jobs: # try this if input-remappers data cannot be found, and set DATA_DIR to a matching directory # find / -type f -name "input-remapper.glade" - sudo -E python tests/test.py --start-dir unit + sudo -E python -m unittest discover tests/unit From 203edb2078bfa9421d6276f23a48ede5e5aad676 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:36:45 +0200 Subject: [PATCH 13/32] kasjdfa --- .github/workflows/test.yml | 2 +- tests/unit/test_daemon.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccc888de0..8fecd6793 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,4 +34,4 @@ jobs: # try this if input-remappers data cannot be found, and set DATA_DIR to a matching directory # find / -type f -name "input-remapper.glade" - sudo -E python -m unittest discover tests/unit + sudo -E python -m unittest discover tests/unit/test_daemon.py diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index 6aec4d154..c3ab3725d 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -17,6 +17,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + from evdev._ecodes import EV_ABS from inputremapper.input_event import InputEvent From 04e5115fa54e91c784f09a27245ca696a99f91c5 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:39:22 +0200 Subject: [PATCH 14/32] akldjsfa --- .github/workflows/test.yml | 2 +- inputremapper/logger/formatter.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8fecd6793..bc34717f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,4 +34,4 @@ jobs: # try this if input-remappers data cannot be found, and set DATA_DIR to a matching directory # find / -type f -name "input-remapper.glade" - sudo -E python -m unittest discover tests/unit/test_daemon.py + sudo -E python -m unittest tests/unit/test_daemon.py diff --git a/inputremapper/logger/formatter.py b/inputremapper/logger/formatter.py index 74b4d1e4b..ac08f63b8 100644 --- a/inputremapper/logger/formatter.py +++ b/inputremapper/logger/formatter.py @@ -23,6 +23,7 @@ import os import sys from datetime import datetime +from typing import Dict class ColorfulFormatter(logging.Formatter): From efdfd857b32f0da8172eed0641e9655aa4680f09 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:56:53 +0200 Subject: [PATCH 15/32] improved test_daemon --- tests/unit/test_daemon.py | 41 ++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index c3ab3725d..e6f01314a 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -18,6 +18,8 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . +from unittest.mock import patch, MagicMock + from evdev._ecodes import EV_ABS from inputremapper.input_event import InputEvent @@ -31,7 +33,6 @@ import os import unittest import time -import subprocess import json import evdev @@ -50,17 +51,12 @@ from inputremapper.injection.global_uinputs import global_uinputs from tests.lib.test_setup import test_setup, is_service_running -check_output = subprocess.check_output -os_system = os.system -dbus_get = type(SystemBus()).get - @test_setup class TestDaemon(unittest.TestCase): new_fixture_path = "/dev/input/event9876" def setUp(self): - self.grab = evdev.InputDevice.grab self.daemon = None PathUtils.mkdir(PathUtils.get_config_path()) global_config._save_config() @@ -74,24 +70,17 @@ def tearDown(self): if self.daemon is not None: self.daemon.stop_all() self.daemon = None - evdev.InputDevice.grab = self.grab - - subprocess.check_output = check_output - os.system = os_system - type(SystemBus()).get = dbus_get cleanup() - def test_connect(self): - os_system_history = [] - os.system = os_system_history.append - + @patch.object(os, "system") + def test_connect(self, os_system_mock: MagicMock): self.assertFalse(is_service_running()) # no daemon runs, should try to run it via pkexec instead. # It fails due to the patch on os.system and therefore exits the process self.assertRaises(SystemExit, Daemon.connect) - self.assertEqual(len(os_system_history), 1) + os_system_mock.assert_called_once() self.assertIsNone(Daemon.connect(False)) # make the connect command work this time by acting like a connection is @@ -100,16 +89,18 @@ def test_connect(self): set_config_dir_callcount = 0 class FakeConnection: - def set_config_dir(self, *args, **kwargs): + def set_config_dir(self, *_, **__): nonlocal set_config_dir_callcount set_config_dir_callcount += 1 - type(SystemBus()).get = lambda *args, **kwargs: FakeConnection() - self.assertIsInstance(Daemon.connect(), FakeConnection) - self.assertEqual(set_config_dir_callcount, 1) + system_bus = SystemBus() + with patch.object(type(system_bus), "get") as get_mock: + get_mock.return_value = FakeConnection() + self.assertIsInstance(Daemon.connect(), FakeConnection) + self.assertEqual(set_config_dir_callcount, 1) - self.assertIsInstance(Daemon.connect(False), FakeConnection) - self.assertEqual(set_config_dir_callcount, 2) + self.assertIsInstance(Daemon.connect(False), FakeConnection) + self.assertEqual(set_config_dir_callcount, 2) def test_daemon(self): # remove the existing system mapping to force our own into it @@ -474,7 +465,8 @@ def test_autoload(self): self.daemon._autoload(group_key) len_after = len(self.daemon.autoload_history._autoload_history) self.assertEqual( - daemon.autoload_history._autoload_history[group_key][1], preset_name + daemon.autoload_history._autoload_history[group_key][1], + preset_name, ) self.assertFalse(daemon.autoload_history.may_autoload(group_key, preset_name)) injector = daemon.injectors[group_key] @@ -483,7 +475,8 @@ def test_autoload(self): # calling duplicate get_autoload does nothing self.daemon._autoload(group_key) self.assertEqual( - daemon.autoload_history._autoload_history[group_key][1], preset_name + daemon.autoload_history._autoload_history[group_key][1], + preset_name, ) self.assertEqual(injector, daemon.injectors[group_key]) self.assertFalse(daemon.autoload_history.may_autoload(group_key, preset_name)) From 2062941335f2f01b954f6b7cf86e6829a967a8c8 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 01:13:48 +0200 Subject: [PATCH 16/32] hopefully fixed test_daemon --- tests/lib/test_setup.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/lib/test_setup.py b/tests/lib/test_setup.py index 222030586..efd86f5d4 100644 --- a/tests/lib/test_setup.py +++ b/tests/lib/test_setup.py @@ -23,6 +23,8 @@ import os import tracemalloc +from inputremapper.configs.global_config import global_config +from inputremapper.configs.paths import PathUtils from tests.lib.cleanup import cleanup, quick_cleanup from tests.lib.fixture_pipes import create_fixture_pipes, remove_fixture_pipes from tests.lib.is_service_running import is_service_running @@ -62,6 +64,14 @@ def setUpClass(): for patch in patches: patch.start() + # TODO if global_config is injected instead, it could work without doing this + # load_config call here. Because right now the constructor uses variables + # that are unpatched once global_config.py is imported. + global_config.path = os.path.join( + PathUtils.config_path(), + "config.json", + ) + original_setUpClass() def tearDownClass(): From 34ccf9c105f1f109c747d56159f983edff3e6813 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 01:21:18 +0200 Subject: [PATCH 17/32] minor cleanup --- bin/input-remapper-control | 6 ++++-- inputremapper/configs/migrations.py | 4 ++-- tests/lib/patches.py | 25 ++++--------------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/bin/input-remapper-control b/bin/input-remapper-control index 498e0d97d..00d67903e 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -135,7 +135,8 @@ class InputRemapperControl: def ensure_migrated(self) -> None: # import stuff late to make sure the correct log level is applied - # before anything is logged TODO fix this + # before anything is logged + # TODO since imports shouldn't run any code, this is fixed by moving towards DI from inputremapper.user import UserUtils if UserUtils.user != "root": @@ -164,7 +165,8 @@ class InputRemapperControl: def _require_group(self, device: str): # import stuff late to make sure the correct log level is applied - # before anything is logged TODO fix this + # before anything is logged + # TODO since imports shouldn't run any code, this is fixed by moving towards DI from inputremapper.groups import groups if device is None: diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index 23cf31a7d..10434a59e 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -51,8 +51,8 @@ from inputremapper.configs.mapping import Mapping, UIMapping from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping, SystemMapping -from inputremapper.injection.global_uinputs import global_uinputs, GlobalUInputs +from inputremapper.configs.system_mapping import system_mapping +from inputremapper.injection.global_uinputs import global_uinputs from inputremapper.injection.macros.parse import is_this_a_macro from inputremapper.logger.logger import logger, VERSION from inputremapper.user import UserUtils diff --git a/tests/lib/patches.py b/tests/lib/patches.py index fde7c7cef..1d34b37da 100644 --- a/tests/lib/patches.py +++ b/tests/lib/patches.py @@ -371,31 +371,14 @@ def hello(self, out: str) -> str: def create_patches(): return [ - # TODO I have to wrap evdev for patches to work, because the order of imports - # still matters until then. + # Sketchy, they only work because the whole modules are imported, instead of + # importing `check_output` and `system` from the module. *patch_evdev(), + patch_os_system(), + patch_check_output(), # Those are comfortably wrapped in a class, and are therefore easy to patch patch_paths(), patch_regrab_timeout(), patch_is_running(), patch_events(), - # Sketchy, they only work because the whole modules are imported, instead of - # importing `check_output` and `system` from the module. - patch_os_system(), - patch_check_output(), ] - - -def apply_all_patches(): - # TODO in the meantime, until evdev is wrapped, this creates and applies patches in - # the right order - for patch in patch_evdev(): - patch.start() - patch_paths().start() - patch_regrab_timeout().start() - patch_is_running().start() - patch_events().start() - patch_os_system().start() - patch_check_output().start() - - # patch_warnings().start() From 28de297a80e7b888c1a05777371cc292176796b2 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 01:23:36 +0200 Subject: [PATCH 18/32] updated docs, removed .run configurations --- .run/All Tests.run.xml | 17 ----------------- .run/Only Integration Tests.run.xml | 17 ----------------- .run/Only Unit Tests.run.xml | 17 ----------------- readme/development.md | 11 ++--------- 4 files changed, 2 insertions(+), 60 deletions(-) delete mode 100644 .run/All Tests.run.xml delete mode 100644 .run/Only Integration Tests.run.xml delete mode 100644 .run/Only Unit Tests.run.xml diff --git a/.run/All Tests.run.xml b/.run/All Tests.run.xml deleted file mode 100644 index e442be10d..000000000 --- a/.run/All Tests.run.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/Only Integration Tests.run.xml b/.run/Only Integration Tests.run.xml deleted file mode 100644 index 704925419..000000000 --- a/.run/Only Integration Tests.run.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.run/Only Unit Tests.run.xml b/.run/Only Unit Tests.run.xml deleted file mode 100644 index 941c9b1ab..000000000 --- a/.run/Only Unit Tests.run.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/readme/development.md b/readme/development.md index 88ea87177..5bf0fd843 100644 --- a/readme/development.md +++ b/readme/development.md @@ -37,22 +37,15 @@ coverage combine && coverage report -m This assumes you are using your system's `pip`. If you are in a virtual env, a `sudo pip install` is not recommened. See [Scripts](#scripts) for alternatives. -Single tests can be executed via `tests/test.py` using the full code path as argment. -``` -python3 tests/test.py tests.unit.test_ipc.TestSocket.test_select -``` - -Also `python -m unittest` can be used, which provides more control over which tests to -run and how to fail on errors. See `python -m unittest -h` for more. ``` +python -m unittest tests/unit/test_daemon.py python -m unittest tests.unit.test_ipc.TestPipe -k "test_pipe" -f +# See `python -m unittest -h` for more. ``` Don't use your computer during integration tests to avoid interacting with the gui, which might make tests fail. -There is also a "run configuration" for PyCharm called "All Tests" included. - To read events for manual testing, `evtest` is very helpful. Add `-d` to `input-remapper-gtk` to get debug output. From e337025c9d85b25e29eae10bd8c407a220b5c20a Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 01:26:45 +0200 Subject: [PATCH 19/32] run all unit tests in github ci --- .github/workflows/test.yml | 2 +- tests/unit/test_reader.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc34717f9..ccc888de0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,4 +34,4 @@ jobs: # try this if input-remappers data cannot be found, and set DATA_DIR to a matching directory # find / -type f -name "input-remapper.glade" - sudo -E python -m unittest tests/unit/test_daemon.py + sudo -E python -m unittest discover tests/unit diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index 38c4f6ce3..3354e56d8 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -99,7 +99,6 @@ def setUp(self): self.reader_client = ReaderClient(self.message_broker, self.groups) def tearDown(self): - quick_cleanup() try: self.reader_client.terminate() except (BrokenPipeError, OSError): From c0c884f83b22263f1a7fe85de1cc998e2ad0f0db Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 01:36:26 +0200 Subject: [PATCH 20/32] removed redundant quick_cleanup calls --- tests/integration/test_components.py | 3 -- tests/integration/test_numlock.py | 9 ----- tests/integration/test_user_interface.py | 2 - tests/lib/test_setup.py | 6 ++- tests/unit/test_config.py | 5 --- tests/unit/test_context.py | 17 ++++---- tests/unit/test_control.py | 18 +++------ tests/unit/test_controller.py | 4 -- tests/unit/test_data_manager.py | 15 +++---- tests/unit/test_event_reader.py | 6 +-- tests/unit/test_groups.py | 9 +---- tests/unit/test_injector.py | 50 +++++++++--------------- tests/unit/test_ipc.py | 12 ++---- tests/unit/test_macros.py | 2 - tests/unit/test_migrations.py | 9 +---- tests/unit/test_paths.py | 9 +---- tests/unit/test_preset.py | 6 +-- tests/unit/test_reader.py | 8 ++-- tests/unit/test_system_mapping.py | 4 -- tests/unit/test_test.py | 23 +++++------ tests/unit/test_user.py | 5 --- 21 files changed, 64 insertions(+), 158 deletions(-) diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index bcca189e3..6d1f23b70 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -33,7 +33,6 @@ gi.require_version("GtkSource", "4") from gi.repository import Gtk, GLib, GtkSource, Gdk -from tests.lib.cleanup import quick_cleanup from tests.lib.spy import spy from tests.lib.logger import logger @@ -118,8 +117,6 @@ def tearDown(self) -> None: # starts without pending events. Gtk.main() - quick_cleanup() - class FlowBoxTestUtils: """Methods to test the FlowBoxes that contain presets and devices. diff --git a/tests/integration/test_numlock.py b/tests/integration/test_numlock.py index e47832a80..d4d7f706c 100644 --- a/tests/integration/test_numlock.py +++ b/tests/integration/test_numlock.py @@ -19,8 +19,6 @@ # along with input-remapper. If not, see . -from tests.lib.cleanup import quick_cleanup - import unittest from inputremapper.injection.numlock import is_numlock_on, set_numlock, ensure_numlock @@ -29,13 +27,6 @@ @test_setup class TestNumlock(unittest.TestCase): - @classmethod - def setUpClass(cls): - quick_cleanup() - - def tearDown(self): - quick_cleanup() - def test_numlock(self): before = is_numlock_on() diff --git a/tests/integration/test_user_interface.py b/tests/integration/test_user_interface.py index 9ab6ae193..37365d124 100644 --- a/tests/integration/test_user_interface.py +++ b/tests/integration/test_user_interface.py @@ -10,7 +10,6 @@ gi.require_version("GtkSource", "4") from gi.repository import Gtk, Gdk, GLib -from tests.lib.cleanup import quick_cleanup from inputremapper.gui.utils import gtk_iteration from inputremapper.gui.messages.message_broker import MessageBroker, MessageType from inputremapper.gui.user_interface import UserInterface @@ -32,7 +31,6 @@ def tearDown(self) -> None: GLib.timeout_add(0, self.user_interface.window.destroy) GLib.timeout_add(0, Gtk.main_quit) Gtk.main() - quick_cleanup() def test_shortcut(self): mock = MagicMock() diff --git a/tests/lib/test_setup.py b/tests/lib/test_setup.py index efd86f5d4..4961128e1 100644 --- a/tests/lib/test_setup.py +++ b/tests/lib/test_setup.py @@ -93,9 +93,13 @@ def setUp(self): def tearDown(self): original_tearDown(self) - # TODO remove quick_cleanup calls from tearDown methods quick_cleanup() + # This assertion was important to me somehow in other tests after the cleanup, + # so I added it to the test setup. I don't remember if this is important to + # check. + assert len(global_config.iterate_autoload_presets()) == 0 + for patch in patches: patch.stop() diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 13ff5261d..f80720eeb 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -25,17 +25,12 @@ from inputremapper.configs.global_config import global_config from inputremapper.configs.paths import PathUtils -from tests.lib.cleanup import quick_cleanup from tests.lib.tmp import tmp from tests.lib.test_setup import test_setup @test_setup class TestConfig(unittest.TestCase): - def tearDown(self): - quick_cleanup() - self.assertEqual(len(global_config.iterate_autoload_presets()), 0) - def test_basic(self): self.assertEqual(global_config.get("a"), None) diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index ffd0f0446..87b6dacd6 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -17,8 +17,9 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from inputremapper.input_event import InputEvent -from tests.lib.cleanup import quick_cleanup + +import unittest + from evdev.ecodes import ( EV_REL, EV_ABS, @@ -27,21 +28,17 @@ REL_WHEEL_HI_RES, REL_HWHEEL_HI_RES, ) -import unittest -from inputremapper.injection.context import Context -from inputremapper.configs.preset import Preset -from inputremapper.configs.mapping import Mapping from inputremapper.configs.input_config import InputCombination +from inputremapper.configs.mapping import Mapping +from inputremapper.configs.preset import Preset +from inputremapper.injection.context import Context +from inputremapper.input_event import InputEvent from tests.lib.test_setup import test_setup @test_setup class TestContext(unittest.TestCase): - @classmethod - def setUpClass(cls): - quick_cleanup() - def test_callbacks(self): preset = Preset() cfg = { diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index 775ab91e1..f2d800385 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -20,24 +20,21 @@ """Testing the input-remapper-control command""" -from unittest.mock import patch - -from tests.lib.cleanup import quick_cleanup -from tests.lib.tmp import tmp - +import collections import os import time import unittest -import collections -from importlib.util import spec_from_loader, module_from_spec from importlib.machinery import SourceFileLoader +from importlib.util import spec_from_loader, module_from_spec +from unittest.mock import patch from inputremapper.configs.global_config import global_config -from inputremapper.daemon import Daemon -from inputremapper.configs.preset import Preset from inputremapper.configs.paths import PathUtils +from inputremapper.configs.preset import Preset +from inputremapper.daemon import Daemon from inputremapper.groups import groups from tests.lib.test_setup import test_setup +from tests.lib.tmp import tmp def import_control(): @@ -70,9 +67,6 @@ class TestControl(unittest.TestCase): def setUp(self): self.input_remapper_control = InputRemapperControl() - def tearDown(self): - quick_cleanup() - def test_autoload(self): device_keys = ["Foo Device 2", "Bar Device"] groups_ = [groups.find(key=key) for key in device_keys] diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index b570ecd9e..39015835d 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -52,7 +52,6 @@ from inputremapper.gui.gettext import _ from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.configs.mapping import UIMapping, MappingData, Mapping -from tests.lib.cleanup import quick_cleanup from tests.lib.spy import spy from tests.lib.patches import FakeDaemonProxy from tests.lib.fixtures import fixtures, prepare_presets @@ -83,9 +82,6 @@ def setUp(self) -> None: self.controller = Controller(self.message_broker, self.data_manager) self.controller.set_gui(self.user_interface) - def tearDown(self) -> None: - quick_cleanup() - def test_should_get_newest_group(self): """get_a_group should the newest group.""" with patch.object( diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index b285b8f64..1a4be4dfc 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -25,11 +25,14 @@ from unittest.mock import MagicMock, call from inputremapper.configs.global_config import global_config +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import UIMapping, MappingData +from inputremapper.configs.paths import PathUtils +from inputremapper.configs.preset import Preset from inputremapper.configs.system_mapping import system_mapping -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.exceptions import DataManagementError from inputremapper.groups import _Groups +from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME from inputremapper.gui.messages.message_broker import ( MessageBroker, MessageType, @@ -40,13 +43,8 @@ ) from inputremapper.gui.reader_client import ReaderClient from inputremapper.injection.global_uinputs import GlobalUInputs -from tests.lib.cleanup import quick_cleanup -from tests.lib.patches import FakeDaemonProxy from tests.lib.fixtures import prepare_presets - -from inputremapper.configs.paths import PathUtils -from inputremapper.configs.preset import Preset -from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME +from tests.lib.patches import FakeDaemonProxy from tests.lib.test_setup import test_setup @@ -74,9 +72,6 @@ def setUp(self) -> None: system_mapping, ) - def tearDown(self) -> None: - quick_cleanup() - def test_load_group_provides_presets(self): """we should get all preset of a group, when loading it""" prepare_presets() diff --git a/tests/unit/test_event_reader.py b/tests/unit/test_event_reader.py index b59a65880..c3e39a3ff 100644 --- a/tests/unit/test_event_reader.py +++ b/tests/unit/test_event_reader.py @@ -36,17 +36,16 @@ REL_WHEEL_HI_RES, ) +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.configs.preset import Preset from inputremapper.configs.system_mapping import system_mapping -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader from inputremapper.injection.global_uinputs import global_uinputs from inputremapper.input_event import InputEvent from inputremapper.utils import get_device_hash from tests.lib.fixtures import fixtures -from tests.lib.cleanup import quick_cleanup from tests.lib.test_setup import test_setup @@ -60,9 +59,6 @@ def setUp(self): global_uinputs.is_service = True global_uinputs.prepare_all() - def tearDown(self): - quick_cleanup() - async def setup(self, source, mapping): """Set a EventReader up for the test and run it in the background.""" context = Context(mapping, {}, {}) diff --git a/tests/unit/test_groups.py b/tests/unit/test_groups.py index 2329315ea..be09a02b2 100644 --- a/tests/unit/test_groups.py +++ b/tests/unit/test_groups.py @@ -19,12 +19,9 @@ # along with input-remapper. If not, see . -from tests.lib.cleanup import quick_cleanup -from tests.lib.fixtures import fixtures, keyboard_keys - +import json import os import unittest -import json import evdev from evdev.ecodes import EV_KEY, KEY_A @@ -37,6 +34,7 @@ DeviceType, _Group, ) +from tests.lib.fixtures import fixtures, keyboard_keys from tests.lib.test_setup import test_setup @@ -49,9 +47,6 @@ def send(self, groups): @test_setup class TestGroups(unittest.TestCase): - def tearDown(self): - quick_cleanup() - def test_group(self): group = _Group( paths=["/dev/a", "/dev/b", "/dev/c"], diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index 9c38afeca..9636b375c 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -23,18 +23,9 @@ except ImportError: from pydantic import ValidationError -from inputremapper.input_event import InputEvent -from tests.lib.patches import uinputs -from tests.lib.cleanup import quick_cleanup -from tests.lib.constants import EVENT_READ_TIMEOUT -from tests.lib.fixtures import fixtures -from tests.lib.pipes import uinput_write_history_pipe -from tests.lib.pipes import read_write_history_pipe, push_events -from tests.lib.fixtures import keyboard_keys - +import time import unittest from unittest import mock -import time import evdev from evdev.ecodes import ( @@ -49,24 +40,31 @@ ABS_VOLUME, ) +from inputremapper.configs.input_config import InputCombination, InputConfig +from inputremapper.configs.mapping import Mapping +from inputremapper.configs.preset import Preset +from inputremapper.configs.system_mapping import ( + system_mapping, + DISABLE_CODE, + DISABLE_NAME, +) +from inputremapper.groups import groups, classify, DeviceType +from inputremapper.injection.context import Context from inputremapper.injection.injector import ( Injector, is_in_capabilities, InjectorState, get_udev_name, ) -from inputremapper.injection.numlock import is_numlock_on -from inputremapper.configs.system_mapping import ( - system_mapping, - DISABLE_CODE, - DISABLE_NAME, -) -from inputremapper.configs.preset import Preset -from inputremapper.configs.mapping import Mapping -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.injection.macros.parse import parse -from inputremapper.injection.context import Context -from inputremapper.groups import groups, classify, DeviceType +from inputremapper.injection.numlock import is_numlock_on +from inputremapper.input_event import InputEvent +from tests.lib.constants import EVENT_READ_TIMEOUT +from tests.lib.fixtures import fixtures +from tests.lib.fixtures import keyboard_keys +from tests.lib.patches import uinputs +from tests.lib.pipes import read_write_history_pipe, push_events +from tests.lib.pipes import uinput_write_history_pipe from tests.lib.test_setup import test_setup @@ -85,7 +83,6 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): def setUpClass(cls): cls.injector = None cls.grab = evdev.InputDevice.grab - quick_cleanup() def setUp(self): self.failed = 0 @@ -109,8 +106,6 @@ def tearDown(self): self.injector = None evdev.InputDevice.grab = self.grab - quick_cleanup() - def initialize_injector(self, group, preset: Preset): self.injector = Injector(group, preset) self.injector._devices = self.injector.group.get_devices() @@ -548,10 +543,6 @@ def test_is_in_capabilities(self): @test_setup class TestModifyCapabilities(unittest.TestCase): - @classmethod - def setUpClass(cls): - quick_cleanup() - def setUp(self): class FakeDevice: def __init__(self): @@ -647,9 +638,6 @@ def check_keys(self, capabilities): self.assertIn(self.shift_l, keys) self.assertNotIn(DISABLE_CODE, keys) - def tearDown(self): - quick_cleanup() - def test_copy_capabilities(self): # I don't know what ABS_VOLUME is, for now I would like to just always # remove it until somebody complains, since its presence broke stuff diff --git a/tests/unit/test_ipc.py b/tests/unit/test_ipc.py index 28c7ddee9..c2fb90f8f 100644 --- a/tests/unit/test_ipc.py +++ b/tests/unit/test_ipc.py @@ -19,19 +19,16 @@ # along with input-remapper. If not, see . import asyncio import multiprocessing - -from tests.lib.cleanup import quick_cleanup -from tests.lib.tmp import tmp - -import unittest +import os import select import time -import os +import unittest from inputremapper.ipc.pipe import Pipe from inputremapper.ipc.shared_dict import SharedDict from inputremapper.ipc.socket import Server, Client, Base from tests.lib.test_setup import test_setup +from tests.lib.tmp import tmp @test_setup @@ -41,9 +38,6 @@ def setUp(self): self.shared_dict.start() time.sleep(0.02) - def tearDown(self): - quick_cleanup() - def test_returns_none(self): self.assertIsNone(self.shared_dict.get("a")) self.assertIsNone(self.shared_dict["a"]) diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index dfe79a6c0..d05b2b78d 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -69,7 +69,6 @@ ) from inputremapper.input_event import InputEvent from tests.lib.logger import logger -from tests.lib.cleanup import quick_cleanup from tests.lib.test_setup import test_setup @@ -89,7 +88,6 @@ def setUp(self): def tearDown(self): self.result = [] - quick_cleanup() def handler(self, type_: int, code: int, value: int): """Where macros should write codes to.""" diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index cc3da078a..b5b622172 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -12,9 +12,6 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from tests.lib.cleanup import quick_cleanup -from tests.lib.tmp import tmp - import json import os import shutil @@ -36,7 +33,6 @@ REL_HWHEEL_HI_RES, ) -from inputremapper.configs.global_config import global_config from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import UIMapping from inputremapper.configs.migrations import Migrations @@ -45,6 +41,7 @@ from inputremapper.logger.logger import VERSION from inputremapper.user import UserUtils from tests.lib.test_setup import test_setup +from tests.lib.tmp import tmp @test_setup @@ -63,10 +60,6 @@ def setUp(self): UserUtils.home, ".config", "input-remapper", "beta_1.6.0-beta" ) - def tearDown(self): - quick_cleanup() - self.assertEqual(len(global_config.iterate_autoload_presets()), 0) - def test_migrate_suffix(self): old = os.path.join(PathUtils.config_path(), "config") new = os.path.join(PathUtils.config_path(), "config.json") diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py index 637f2c902..c24c80e7d 100644 --- a/tests/unit/test_paths.py +++ b/tests/unit/test_paths.py @@ -19,15 +19,13 @@ # along with input-remapper. If not, see . -from tests.lib.cleanup import quick_cleanup -from tests.lib.tmp import tmp - import os -import unittest import tempfile +import unittest from inputremapper.configs.paths import PathUtils from tests.lib.test_setup import test_setup +from tests.lib.tmp import tmp def _raise(error): @@ -36,9 +34,6 @@ def _raise(error): @test_setup class TestPaths(unittest.TestCase): - def tearDown(self): - quick_cleanup() - def test_touch(self): with tempfile.TemporaryDirectory() as local_tmp: path_abcde = os.path.join(local_tmp, "a/b/c/d/e") diff --git a/tests/unit/test_preset.py b/tests/unit/test_preset.py index 2a72b699a..ea4e96983 100644 --- a/tests/unit/test_preset.py +++ b/tests/unit/test_preset.py @@ -24,12 +24,11 @@ from evdev.ecodes import EV_KEY, EV_ABS +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.configs.mapping import UIMapping from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.input_config import InputCombination, InputConfig -from tests.lib.cleanup import quick_cleanup from tests.lib.test_setup import test_setup @@ -39,9 +38,6 @@ def setUp(self): self.preset = Preset(PathUtils.get_preset_path("foo", "bar2")) self.assertFalse(self.preset.has_unsaved_changes()) - def tearDown(self): - quick_cleanup() - def test_is_mapped_multiple_times(self): combination = InputCombination( InputCombination.from_tuples((1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4)) diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index 3354e56d8..50ba933a9 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -19,9 +19,9 @@ # along with input-remapper. If not, see . import asyncio -import os import json import multiprocessing +import os import time import unittest from typing import List, Optional @@ -53,16 +53,15 @@ from inputremapper.gui.reader_client import ReaderClient from inputremapper.gui.reader_service import ReaderService, ContextDummy from inputremapper.input_event import InputEvent -from tests.lib.fixtures import new_event -from tests.lib.cleanup import quick_cleanup from tests.lib.constants import ( EVENT_READ_TIMEOUT, START_READING_DELAY, MAX_ABS, MIN_ABS, ) -from tests.lib.pipes import push_event, push_events from tests.lib.fixtures import fixtures +from tests.lib.fixtures import new_event +from tests.lib.pipes import push_event, push_events from tests.lib.spy import spy from tests.lib.test_setup import test_setup @@ -167,7 +166,6 @@ def setUp(self): self.reader_client = ReaderClient(self.message_broker, self.groups) def tearDown(self): - quick_cleanup() try: self.reader_client.terminate() except (BrokenPipeError, OSError): diff --git a/tests/unit/test_system_mapping.py b/tests/unit/test_system_mapping.py index 678f72ba3..3b2a5d462 100644 --- a/tests/unit/test_system_mapping.py +++ b/tests/unit/test_system_mapping.py @@ -28,15 +28,11 @@ from inputremapper.configs.paths import PathUtils from inputremapper.configs.system_mapping import SystemMapping, XMODMAP_FILENAME -from tests.lib.cleanup import quick_cleanup from tests.lib.test_setup import test_setup @test_setup class TestSystemMapping(unittest.TestCase): - def tearDown(self): - quick_cleanup() - def test_update(self): system_mapping = SystemMapping() system_mapping.update({"foo1": 101, "bar1": 102}) diff --git a/tests/unit/test_test.py b/tests/unit/test_test.py index 0a553a47a..92a813b81 100644 --- a/tests/unit/test_test.py +++ b/tests/unit/test_test.py @@ -18,28 +18,26 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . import asyncio - -from tests.lib.cleanup import cleanup, quick_cleanup -from tests.lib.constants import EVENT_READ_TIMEOUT, START_READING_DELAY -from tests.lib.logger import logger -from tests.lib.fixtures import fixtures -from tests.lib.pipes import push_events -from tests.lib.patches import InputDevice - +import multiprocessing import os -import unittest import time -import multiprocessing +import unittest import evdev from evdev.ecodes import EV_ABS, EV_KEY from inputremapper.groups import groups, _Groups +from inputremapper.gui.messages.message_broker import MessageBroker from inputremapper.gui.reader_client import ReaderClient from inputremapper.gui.reader_service import ReaderService from inputremapper.input_event import InputEvent from inputremapper.utils import get_device_hash -from inputremapper.gui.messages.message_broker import MessageBroker +from tests.lib.cleanup import cleanup +from tests.lib.constants import EVENT_READ_TIMEOUT, START_READING_DELAY +from tests.lib.fixtures import fixtures +from tests.lib.logger import logger +from tests.lib.patches import InputDevice +from tests.lib.pipes import push_events from tests.lib.test_setup import test_setup @@ -48,9 +46,6 @@ class TestTest(unittest.TestCase): def test_stubs(self): self.assertIsNotNone(groups.find(key="Foo Device 2")) - def tearDown(self): - quick_cleanup() - def test_fake_capabilities(self): device = InputDevice("/dev/input/event30") capabilities = device.capabilities(absinfo=False) diff --git a/tests/unit/test_user.py b/tests/unit/test_user.py index 591b14561..d9e35f09c 100644 --- a/tests/unit/test_user.py +++ b/tests/unit/test_user.py @@ -19,8 +19,6 @@ # along with input-remapper. If not, see . -from tests.lib.cleanup import quick_cleanup - import os import unittest from unittest import mock @@ -35,9 +33,6 @@ def _raise(error): @test_setup class TestUser(unittest.TestCase): - def tearDown(self): - quick_cleanup() - def test_get_user(self): with mock.patch("os.getlogin", lambda: "foo"): self.assertEqual(UserUtils.get_user(), "foo") From f2457ebd4fcef52c27418ed7e0f55a4fb1e751c1 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 01:39:19 +0200 Subject: [PATCH 21/32] minor cleanup stuff --- tests/integration/test_daemon.py | 7 ++++--- tests/unit/test_data_manager.py | 1 + .../test_event_pipeline/test_axis_transformation.py | 3 ++- tests/unit/test_event_pipeline/test_event_pipeline.py | 1 + tests/unit/test_global_uinputs.py | 10 +++++----- tests/unit/test_ipc.py | 1 + tests/unit/test_test.py | 1 + 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/integration/test_daemon.py b/tests/integration/test_daemon.py index 796840367..cf8e4e669 100644 --- a/tests/integration/test_daemon.py +++ b/tests/integration/test_daemon.py @@ -17,15 +17,16 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from tests.lib.test_setup import is_service_running -import os import multiprocessing -import unittest +import os import time +import unittest import gi +from tests.lib.test_setup import is_service_running + gi.require_version("Gtk", "3.0") from gi.repository import Gtk diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index 1a4be4dfc..18c5873b3 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -17,6 +17,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + import os import time import unittest diff --git a/tests/unit/test_event_pipeline/test_axis_transformation.py b/tests/unit/test_event_pipeline/test_axis_transformation.py index 180174369..08867df64 100644 --- a/tests/unit/test_event_pipeline/test_axis_transformation.py +++ b/tests/unit/test_event_pipeline/test_axis_transformation.py @@ -17,10 +17,11 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + import dataclasses import functools -import unittest import itertools +import unittest from typing import Iterable, List from inputremapper.injection.mapping_handlers.axis_transform import Transformation diff --git a/tests/unit/test_event_pipeline/test_event_pipeline.py b/tests/unit/test_event_pipeline/test_event_pipeline.py index db7583793..97271993c 100644 --- a/tests/unit/test_event_pipeline/test_event_pipeline.py +++ b/tests/unit/test_event_pipeline/test_event_pipeline.py @@ -17,6 +17,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + import asyncio import unittest from typing import Iterable diff --git a/tests/unit/test_global_uinputs.py b/tests/unit/test_global_uinputs.py index 817f9fc9c..458b8c1a7 100644 --- a/tests/unit/test_global_uinputs.py +++ b/tests/unit/test_global_uinputs.py @@ -17,25 +17,25 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from inputremapper.input_event import InputEvent -from tests.lib.cleanup import cleanup import sys import unittest -import evdev - from unittest.mock import patch + +import evdev from evdev.ecodes import ( KEY_A, ABS_X, ) +from inputremapper.exceptions import EventNotHandled, UinputNotAvailable from inputremapper.injection.global_uinputs import ( global_uinputs, FrontendUInput, GlobalUInputs, ) -from inputremapper.exceptions import EventNotHandled, UinputNotAvailable +from inputremapper.input_event import InputEvent +from tests.lib.cleanup import cleanup from tests.lib.test_setup import test_setup diff --git a/tests/unit/test_ipc.py b/tests/unit/test_ipc.py index c2fb90f8f..70fc47188 100644 --- a/tests/unit/test_ipc.py +++ b/tests/unit/test_ipc.py @@ -17,6 +17,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + import asyncio import multiprocessing import os diff --git a/tests/unit/test_test.py b/tests/unit/test_test.py index 92a813b81..913eb1912 100644 --- a/tests/unit/test_test.py +++ b/tests/unit/test_test.py @@ -17,6 +17,7 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . + import asyncio import multiprocessing import os From bb491eea3a91851f2cc772b18f432283e7a6c530 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:00:37 +0200 Subject: [PATCH 22/32] bump python version in ci --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccc888de0..6faec95a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository strategy: matrix: - python-version: ["3.7", "3.11"] # min and max supported versions? + python-version: ["3.8", "3.12"] # min and max supported versions? steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} From c1d4e5897d3aaa66d29e881933f646621d6422c0 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:57:10 +0200 Subject: [PATCH 23/32] using pythons patch method everywhere else as well --- tests/integration/test_data.py | 33 ++++---- tests/integration/test_gui.py | 139 ++++++++++++++++++--------------- tests/unit/test_groups.py | 1 - tests/unit/test_reader.py | 5 +- 4 files changed, 97 insertions(+), 81 deletions(-) diff --git a/tests/integration/test_data.py b/tests/integration/test_data.py index ee2ccde94..9ed77a844 100644 --- a/tests/integration/test_data.py +++ b/tests/integration/test_data.py @@ -18,34 +18,33 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . - -import unittest import os +import unittest +from unittest.mock import patch + import pkg_resources from inputremapper.configs.data import get_data_path from tests.lib.test_setup import test_setup +egg_info_distribution = pkg_resources.require("input-remapper")[0] -@test_setup -class TestData(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.original_location = pkg_resources.require("input-remapper")[0].location +project_root = os.getcwd().replace("/tests/integration", "") - def tearDown(self): - pkg_resources.require("input-remapper")[0].location = self.original_location +@test_setup +class TestData(unittest.TestCase): + @patch.object(egg_info_distribution, "location", project_root) def test_data_editable(self): - path = os.getcwd().replace("/tests/integration", "") - pkg_resources.require("input-remapper")[0].location = path - self.assertEqual(get_data_path(), path + "/data/") - self.assertEqual(get_data_path("a"), path + "/data/a") - + self.assertEqual(get_data_path(), project_root + "/data/") + self.assertEqual(get_data_path("a"), project_root + "/data/a") + + @patch.object( + egg_info_distribution, + "location", + "/usr/some/where/python3.8/dist-packages/", + ) def test_data_usr(self): - path = "/usr/some/where/python3.8/dist-packages/" - pkg_resources.require("input-remapper")[0].location = path - self.assertTrue(get_data_path().startswith("/usr/")) self.assertTrue(get_data_path().endswith("input-remapper/")) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index dbafe30ca..be79a778f 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -139,28 +139,31 @@ def process(): multiprocessing.Process(target=process).start() +def os_system_patch(cmd, original_os_system=os.system): + # instead of running pkexec, fork instead. This will make + # the reader-service aware of all the test patches + if "pkexec input-remapper-control --command start-reader-service" in cmd: + logger.info("pkexec-patch starting ReaderService process") + start_reader_service() + return 0 + + return original_os_system(cmd) + + @contextmanager def patch_launch(): """patch the launch function such that we don't connect to the dbus and don't use pkexec to start the reader-service""" - original_connect = Daemon.connect - original_os_system = os.system - Daemon.connect = Daemon - - def os_system(cmd): - # instead of running pkexec, fork instead. This will make - # the reader-service aware of all the test patches - if "pkexec input-remapper-control --command start-reader-service" in cmd: - logger.info("pkexec-patch starting ReaderService process") - start_reader_service() - return 0 - - return original_os_system(cmd) - - os.system = os_system - yield - os.system = original_os_system - Daemon.connect = original_connect + with patch.object( + os, + "system", + os_system_patch, + ), patch.object( + Daemon, + "connect", + Daemon, + ): + yield def clean_up_integration(test): @@ -188,18 +191,8 @@ def get_keyval(self): @test_setup class TestGroupsFromReaderService(unittest.TestCase): - def setUp(self): - # don't try to connect, return an object instance of it instead - self.original_connect = Daemon.connect - Daemon.connect = Daemon - - # this is already part of the test. we need a bit of patching and hacking - # because we want to discover the groups as early a possible, to reduce startup - # time for the application - self.original_os_system = os.system - self.reader_service_started = MagicMock() - - def os_system(cmd): + def patch_os_system(self): + def os_system(cmd, original_os_system=os.system): # instead of running pkexec, fork instead. This will make # the reader-service aware of all the test patches if "pkexec input-remapper-control --command start-reader-service" in cmd: @@ -207,9 +200,33 @@ def os_system(cmd): self.reader_service_started() return 0 - return self.original_os_system(cmd) + return original_os_system(cmd) + + self.os_system_patch = patch.object( + os, + "system", + os_system, + ) + + # this is already part of the test. we need a bit of patching and hacking + # because we want to discover the groups as early a possible, to reduce startup + # time for the application + self.os_system_patch.start() + + def patch_daemon(self): + # don't try to connect, return an object instance of it instead + self.daemon_connect_patch = patch.object( + Daemon, + "connect", + Daemon, + ) + self.daemon_connect_patch.start() + + def setUp(self): + self.reader_service_started = MagicMock() + self.patch_os_system() + self.patch_daemon() - os.system = os_system ( self.user_interface, self.controller, @@ -220,8 +237,8 @@ def os_system(cmd): def tearDown(self): clean_up_integration(self) - os.system = self.original_os_system - Daemon.connect = self.original_connect + self.os_system_patch.stop() + self.daemon_connect_patch.stop() def test_knows_devices(self): # verify that it is working as expected. The gui doesn't have knowledge @@ -246,37 +263,37 @@ def test_knows_devices(self): self.assertEqual(self.data_manager.active_group.name, "Foo Device") -class PatchedConfirmDelete: - def __init__(self, user_interface: UserInterface, response=Gtk.ResponseType.ACCEPT): - self.response = response - self.user_interface = user_interface - self._original_create_dialog = user_interface._create_dialog - self.patch = None +@contextmanager +def patch_confirm_delete( + user_interface: UserInterface, + response=Gtk.ResponseType.ACCEPT, +): + original_create_dialog = user_interface._create_dialog - def _create_dialog_patch(self, *args, **kwargs): + def _create_dialog_patch(*args, **kwargs): """A patch for the deletion confirmation that briefly shows the dialog.""" - confirm_cancel_dialog = self._original_create_dialog(*args, **kwargs) + confirm_cancel_dialog = original_create_dialog(*args, **kwargs) + # the emitted signal causes the dialog to close GLib.timeout_add( 100, - lambda: confirm_cancel_dialog.emit("response", self.response), + lambda: confirm_cancel_dialog.emit("response", response), ) - Gtk.MessageDialog.run(confirm_cancel_dialog) # don't recursively call the patch - confirm_cancel_dialog.run = lambda: self.response + # don't recursively call the patch + Gtk.MessageDialog.run(confirm_cancel_dialog) - return confirm_cancel_dialog + confirm_cancel_dialog.run = lambda: response - def __enter__(self): - self.patch = patch.object( - self.user_interface, - "_create_dialog", - self._create_dialog_patch, - ) - self.patch.__enter__() + return confirm_cancel_dialog - def __exit__(self, *args, **kwargs): - self.patch.__exit__(*args, **kwargs) + with patch.object( + user_interface, + "_create_dialog", + _create_dialog_patch, + ): + # Tests are run during `yield` + yield class GuiTestBase(unittest.TestCase): @@ -1396,7 +1413,7 @@ def test_remove_mapping(self): self.assertEqual(len(self.data_manager.active_preset), 2) self.assertEqual(len(self.selection_label_listbox.get_children()), 2) - with PatchedConfirmDelete(self.user_interface): + with patch_confirm_delete(self.user_interface): self.delete_mapping_btn.clicked() gtk_iteration() @@ -1455,7 +1472,7 @@ def save(): status = self.get_status_text() self.assertIn("Permission denied", status) - with PatchedConfirmDelete(self.user_interface): + with patch_confirm_delete(self.user_interface): self.delete_preset_btn.clicked() gtk_iteration() self.assertFalse(os.path.exists(preset_path)) @@ -1885,7 +1902,7 @@ def test_delete_preset(self): os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3")) ) - with PatchedConfirmDelete(self.user_interface, Gtk.ResponseType.CANCEL): + with patch_confirm_delete(self.user_interface, Gtk.ResponseType.CANCEL): self.delete_preset_btn.clicked() gtk_iteration() self.assertTrue( @@ -1894,7 +1911,7 @@ def test_delete_preset(self): self.assertEqual(self.data_manager.active_preset.name, "preset3") self.assertEqual(self.data_manager.active_group.name, "Foo Device") - with PatchedConfirmDelete(self.user_interface): + with patch_confirm_delete(self.user_interface): self.delete_preset_btn.clicked() gtk_iteration() self.assertFalse( @@ -1975,7 +1992,7 @@ def test_shared_presets(self): self.assertNotEqual(presets1, presets3) def test_delete_last_preset(self): - with PatchedConfirmDelete(self.user_interface): + with patch_confirm_delete(self.user_interface): # as per test_initial_state we already have preset3 loaded self.assertEqual(self.data_manager.active_preset.name, "preset3") @@ -2032,7 +2049,7 @@ def test_enable_disable_output(self): self.assertTrue(self.output_box.get_sensitive()) # disable it by deleting the mapping - with PatchedConfirmDelete(self.user_interface): + with patch_confirm_delete(self.user_interface): self.delete_mapping_btn.clicked() gtk_iteration() diff --git a/tests/unit/test_groups.py b/tests/unit/test_groups.py index be09a02b2..9dc5b887f 100644 --- a/tests/unit/test_groups.py +++ b/tests/unit/test_groups.py @@ -18,7 +18,6 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . - import json import os import unittest diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index 50ba933a9..c0e0b5e46 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -126,8 +126,9 @@ def remember_context(*args, **kwargs): context = original_create_event_pipeline(*args, **kwargs) return context - with mock.patch( - "inputremapper.gui.reader_service.ReaderService._create_event_pipeline", + with mock.patch.object( + ReaderService, + "_create_event_pipeline", remember_context, ): await self.create_reader_service() From 4f03e57c8bbffb7152dd85527f7705a467af06a6 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:24:26 +0200 Subject: [PATCH 24/32] rename logger to logging --- bin/input-remapper-control | 2 +- bin/input-remapper-gtk | 2 +- bin/input-remapper-reader-service | 2 +- bin/input-remapper-service | 2 +- inputremapper/configs/base_config.py | 2 +- inputremapper/configs/data.py | 2 +- inputremapper/configs/global_config.py | 2 +- inputremapper/configs/input_config.py | 2 +- inputremapper/configs/migrations.py | 2 +- inputremapper/configs/paths.py | 2 +- inputremapper/configs/preset.py | 2 +- inputremapper/configs/system_mapping.py | 2 +- inputremapper/daemon.py | 2 +- inputremapper/groups.py | 2 +- inputremapper/gui/autocompletion.py | 2 +- inputremapper/gui/components/device_groups.py | 2 +- inputremapper/gui/components/presets.py | 2 +- inputremapper/gui/controller.py | 2 +- inputremapper/gui/data_manager.py | 2 +- inputremapper/gui/messages/message_broker.py | 2 +- inputremapper/gui/reader_client.py | 2 +- inputremapper/gui/reader_service.py | 2 +- inputremapper/gui/user_interface.py | 2 +- inputremapper/gui/utils.py | 2 +- inputremapper/injection/context.py | 2 +- inputremapper/injection/event_reader.py | 2 +- inputremapper/injection/global_uinputs.py | 2 +- inputremapper/injection/injector.py | 2 +- inputremapper/injection/macros/macro.py | 2 +- inputremapper/injection/macros/parse.py | 2 +- inputremapper/injection/mapping_handlers/abs_to_abs_handler.py | 2 +- inputremapper/injection/mapping_handlers/abs_to_rel_handler.py | 2 +- inputremapper/injection/mapping_handlers/axis_switch_handler.py | 2 +- inputremapper/injection/mapping_handlers/combination_handler.py | 2 +- inputremapper/injection/mapping_handlers/key_handler.py | 2 +- inputremapper/injection/mapping_handlers/macro_handler.py | 2 +- inputremapper/injection/mapping_handlers/mapping_handler.py | 2 +- inputremapper/injection/mapping_handlers/mapping_parser.py | 2 +- inputremapper/injection/mapping_handlers/rel_to_abs_handler.py | 2 +- inputremapper/injection/mapping_handlers/rel_to_btn_handler.py | 2 +- inputremapper/injection/mapping_handlers/rel_to_rel_handler.py | 2 +- inputremapper/injection/numlock.py | 2 +- inputremapper/ipc/pipe.py | 2 +- inputremapper/ipc/shared_dict.py | 2 +- inputremapper/ipc/socket.py | 2 +- inputremapper/{logger => logging}/__init__.py | 0 inputremapper/{logger => logging}/formatter.py | 0 inputremapper/{logger => logging}/logger.py | 2 +- tests/lib/logger.py | 2 +- tests/unit/test_logger.py | 2 +- tests/unit/test_migrations.py | 2 +- 51 files changed, 49 insertions(+), 49 deletions(-) rename inputremapper/{logger => logging}/__init__.py (100%) rename inputremapper/{logger => logging}/formatter.py (100%) rename inputremapper/{logger => logging}/logger.py (98%) diff --git a/bin/input-remapper-control b/bin/input-remapper-control index 00d67903e..499e1262d 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -31,7 +31,7 @@ from typing import Union, Optional from inputremapper.configs.global_config import global_config from inputremapper.configs.migrations import Migrations -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger # TODO this sounds like something DI would fix, making everything independent of diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index 6b10859b7..40cc25630 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -39,7 +39,7 @@ Gtk.init() from inputremapper.gui.gettext import _, LOCALE_DIR from inputremapper.gui.reader_service import ReaderService from inputremapper.daemon import DaemonProxy -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger def start_processes() -> DaemonProxy: diff --git a/bin/input-remapper-reader-service b/bin/input-remapper-reader-service index be5838c65..e564b0426 100755 --- a/bin/input-remapper-reader-service +++ b/bin/input-remapper-reader-service @@ -28,7 +28,7 @@ import sys from argparse import ArgumentParser from inputremapper.groups import _Groups -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger if __name__ == "__main__": parser = ArgumentParser() diff --git a/bin/input-remapper-service b/bin/input-remapper-service index 180453dff..7e3cc2d9e 100755 --- a/bin/input-remapper-service +++ b/bin/input-remapper-service @@ -25,7 +25,7 @@ import sys from argparse import ArgumentParser -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger if __name__ == "__main__": parser = ArgumentParser() diff --git a/inputremapper/configs/base_config.py b/inputremapper/configs/base_config.py index 6b10726e6..a6a4407e6 100644 --- a/inputremapper/configs/base_config.py +++ b/inputremapper/configs/base_config.py @@ -22,7 +22,7 @@ import copy from typing import Union, List, Optional, Callable, Any -from inputremapper.logger.logger import logger, VERSION +from inputremapper.logging.logger import logger, VERSION NONE = "none" diff --git a/inputremapper/configs/data.py b/inputremapper/configs/data.py index 93a4652d9..a1eca35dc 100644 --- a/inputremapper/configs/data.py +++ b/inputremapper/configs/data.py @@ -27,7 +27,7 @@ import pkg_resources -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger logged = False diff --git a/inputremapper/configs/global_config.py b/inputremapper/configs/global_config.py index cb3579453..3c08a5cd0 100644 --- a/inputremapper/configs/global_config.py +++ b/inputremapper/configs/global_config.py @@ -26,7 +26,7 @@ from inputremapper.configs.base_config import ConfigBase, INITIAL_CONFIG from inputremapper.configs.paths import PathUtils from inputremapper.user import UserUtils -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger MOUSE = "mouse" WHEEL = "wheel" diff --git a/inputremapper/configs/input_config.py b/inputremapper/configs/input_config.py index 009bf9a2f..5648859a7 100644 --- a/inputremapper/configs/input_config.py +++ b/inputremapper/configs/input_config.py @@ -33,7 +33,7 @@ from inputremapper.configs.system_mapping import system_mapping from inputremapper.gui.messages.message_types import MessageType -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import get_evdev_constant_name # having shift in combinations modifies the configured output, diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index 10434a59e..877efe75a 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -54,7 +54,7 @@ from inputremapper.configs.system_mapping import system_mapping from inputremapper.injection.global_uinputs import global_uinputs from inputremapper.injection.macros.parse import is_this_a_macro -from inputremapper.logger.logger import logger, VERSION +from inputremapper.logging.logger import logger, VERSION from inputremapper.user import UserUtils diff --git a/inputremapper/configs/paths.py b/inputremapper/configs/paths.py index b9183d6b6..c450619c4 100644 --- a/inputremapper/configs/paths.py +++ b/inputremapper/configs/paths.py @@ -26,7 +26,7 @@ import shutil from typing import List, Union, Optional -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.user import UserUtils diff --git a/inputremapper/configs/preset.py b/inputremapper/configs/preset.py index f637b236d..973d11f39 100644 --- a/inputremapper/configs/preset.py +++ b/inputremapper/configs/preset.py @@ -44,7 +44,7 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping, UIMapping from inputremapper.configs.paths import PathUtils -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger MappingModel = TypeVar("MappingModel", bound=UIMapping) diff --git a/inputremapper/configs/system_mapping.py b/inputremapper/configs/system_mapping.py index 86d710d7f..f88493b3a 100644 --- a/inputremapper/configs/system_mapping.py +++ b/inputremapper/configs/system_mapping.py @@ -26,7 +26,7 @@ import evdev from inputremapper.configs.paths import PathUtils -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import is_service DISABLE_NAME = "disable" diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index 75c04f5a1..cf949e9cd 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -38,7 +38,7 @@ gi.require_version("GLib", "2.0") from gi.repository import GLib -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.injection.injector import Injector, InjectorState from inputremapper.configs.preset import Preset from inputremapper.configs.global_config import global_config diff --git a/inputremapper/groups.py b/inputremapper/groups.py index 9a1865353..7a48cb297 100644 --- a/inputremapper/groups.py +++ b/inputremapper/groups.py @@ -55,7 +55,7 @@ ) from inputremapper.configs.paths import PathUtils -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import get_device_hash TABLET_KEYS = [ diff --git a/inputremapper/gui/autocompletion.py b/inputremapper/gui/autocompletion.py index c3c1df99f..455be58cb 100644 --- a/inputremapper/gui/autocompletion.py +++ b/inputremapper/gui/autocompletion.py @@ -39,7 +39,7 @@ get_macro_argument_names, remove_comments, ) -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger # no deprecated shorthand function-names FUNCTION_NAMES = [name for name in TASK_FACTORIES.keys() if len(name) > 1] diff --git a/inputremapper/gui/components/device_groups.py b/inputremapper/gui/components/device_groups.py index 274f9fb2d..01c994c76 100644 --- a/inputremapper/gui/components/device_groups.py +++ b/inputremapper/gui/components/device_groups.py @@ -36,7 +36,7 @@ GroupData, DoStackSwitch, ) -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger class DeviceGroupEntry(FlowBoxEntry): diff --git a/inputremapper/gui/components/presets.py b/inputremapper/gui/components/presets.py index a8dc05b32..9adc1c08a 100644 --- a/inputremapper/gui/components/presets.py +++ b/inputremapper/gui/components/presets.py @@ -37,7 +37,7 @@ PresetData, DoStackSwitch, ) -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger class PresetEntry(FlowBoxEntry): diff --git a/inputremapper/gui/controller.py b/inputremapper/gui/controller.py index 20c68cafa..5b8e9fb87 100644 --- a/inputremapper/gui/controller.py +++ b/inputremapper/gui/controller.py @@ -67,7 +67,7 @@ InjectorState, InjectorStateMessage, ) -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger if TYPE_CHECKING: # avoids gtk import error in tests diff --git a/inputremapper/gui/data_manager.py b/inputremapper/gui/data_manager.py index 2a6ab09d2..f4d89feae 100644 --- a/inputremapper/gui/data_manager.py +++ b/inputremapper/gui/data_manager.py @@ -50,7 +50,7 @@ InjectorState, InjectorStateMessage, ) -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger DEFAULT_PRESET_NAME = _("new preset") diff --git a/inputremapper/gui/messages/message_broker.py b/inputremapper/gui/messages/message_broker.py index 9c5d08fb2..13ed7e57b 100644 --- a/inputremapper/gui/messages/message_broker.py +++ b/inputremapper/gui/messages/message_broker.py @@ -33,7 +33,7 @@ ) from inputremapper.gui.messages.message_types import MessageType -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger if TYPE_CHECKING: pass diff --git a/inputremapper/gui/reader_client.py b/inputremapper/gui/reader_client.py index 8e4e880f1..91dd99429 100644 --- a/inputremapper/gui/reader_client.py +++ b/inputremapper/gui/reader_client.py @@ -51,7 +51,7 @@ from inputremapper.gui.utils import CTX_ERROR from inputremapper.input_event import InputEvent from inputremapper.ipc.pipe import Pipe -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger BLACKLISTED_EVENTS = [(1, evdev.ecodes.BTN_TOOL_DOUBLETAP)] RecordingGenerator = Generator[None, InputEvent, None] diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index d8247eec9..b0180e38d 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -64,7 +64,7 @@ from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler from inputremapper.input_event import InputEvent, EventActions from inputremapper.ipc.pipe import Pipe -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.user import UserUtils from inputremapper.utils import get_device_hash diff --git a/inputremapper/gui/user_interface.py b/inputremapper/gui/user_interface.py index ab8cab489..46b63be4a 100644 --- a/inputremapper/gui/user_interface.py +++ b/inputremapper/gui/user_interface.py @@ -62,7 +62,7 @@ gtk_iteration, ) from inputremapper.injection.injector import InjectorStateMessage -from inputremapper.logger.logger import logger, COMMIT_HASH, VERSION, EVDEV_VERSION +from inputremapper.logging.logger import logger, COMMIT_HASH, VERSION, EVDEV_VERSION # https://cjenkins.wordpress.com/2012/05/08/use-gtksourceview-widget-in-glade/ GObject.type_register(GtkSource.View) diff --git a/inputremapper/gui/utils.py b/inputremapper/gui/utils.py index d2d7530dd..15122514d 100644 --- a/inputremapper/gui/utils.py +++ b/inputremapper/gui/utils.py @@ -25,7 +25,7 @@ from gi.repository import Gtk, GLib, Gdk -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger # status ctx ids diff --git a/inputremapper/injection/context.py b/inputremapper/injection/context.py index 16e7afa12..cf56608b1 100644 --- a/inputremapper/injection/context.py +++ b/inputremapper/injection/context.py @@ -38,7 +38,7 @@ EventPipelines, ) from inputremapper.input_event import InputEvent -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger class Context: diff --git a/inputremapper/injection/event_reader.py b/inputremapper/injection/event_reader.py index 296793272..218e4049b 100644 --- a/inputremapper/injection/event_reader.py +++ b/inputremapper/injection/event_reader.py @@ -32,7 +32,7 @@ NotifyCallback, ) from inputremapper.input_event import InputEvent -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import get_device_hash, DeviceHash diff --git a/inputremapper/injection/global_uinputs.py b/inputremapper/injection/global_uinputs.py index 6c92008c8..3fba120c7 100644 --- a/inputremapper/injection/global_uinputs.py +++ b/inputremapper/injection/global_uinputs.py @@ -23,7 +23,7 @@ import inputremapper.exceptions import inputremapper.utils -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger MIN_ABS = -(2**15) # -32768 MAX_ABS = 2**15 # 32768 diff --git a/inputremapper/injection/injector.py b/inputremapper/injection/injector.py index c49a0013c..35db52f49 100644 --- a/inputremapper/injection/injector.py +++ b/inputremapper/injection/injector.py @@ -44,7 +44,7 @@ from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader from inputremapper.injection.numlock import set_numlock, is_numlock_on, ensure_numlock -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import get_device_hash CapabilitiesDict = Dict[int, List[int]] diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index 59bd36aca..b0d3fd0e9 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -61,7 +61,7 @@ ) from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.ipc.shared_dict import SharedDict -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger Handler = Callable[[Tuple[int, int, int]], None] MacroTask = Callable[[Handler], Awaitable] diff --git a/inputremapper/injection/macros/parse.py b/inputremapper/injection/macros/parse.py index 646d094f5..13af186cb 100644 --- a/inputremapper/injection/macros/parse.py +++ b/inputremapper/injection/macros/parse.py @@ -27,7 +27,7 @@ from inputremapper.configs.validation_errors import MacroParsingError from inputremapper.injection.macros.macro import Macro, Variable -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger def is_this_a_macro(output: Any): diff --git a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py index aa57e1523..2159896fe 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py @@ -33,7 +33,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import get_evdev_constant_name diff --git a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py index a73f8f1d9..fe323b1a0 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py @@ -49,7 +49,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import get_evdev_constant_name diff --git a/inputremapper/injection/mapping_handlers/axis_switch_handler.py b/inputremapper/injection/mapping_handlers/axis_switch_handler.py index ae268f6d4..f96b1fc0e 100644 --- a/inputremapper/injection/mapping_handlers/axis_switch_handler.py +++ b/inputremapper/injection/mapping_handlers/axis_switch_handler.py @@ -31,7 +31,7 @@ ContextProtocol, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import get_device_hash diff --git a/inputremapper/injection/mapping_handlers/combination_handler.py b/inputremapper/injection/mapping_handlers/combination_handler.py index 50a971a2f..0ec784867 100644 --- a/inputremapper/injection/mapping_handlers/combination_handler.py +++ b/inputremapper/injection/mapping_handlers/combination_handler.py @@ -32,7 +32,7 @@ HandlerEnums, ) from inputremapper.input_event import InputEvent -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger if TYPE_CHECKING: from inputremapper.injection.context import Context diff --git a/inputremapper/injection/mapping_handlers/key_handler.py b/inputremapper/injection/mapping_handlers/key_handler.py index 26ad0adb2..7ce023fb4 100644 --- a/inputremapper/injection/mapping_handlers/key_handler.py +++ b/inputremapper/injection/mapping_handlers/key_handler.py @@ -29,7 +29,7 @@ HandlerEnums, ) from inputremapper.input_event import InputEvent -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import get_evdev_constant_name diff --git a/inputremapper/injection/mapping_handlers/macro_handler.py b/inputremapper/injection/mapping_handlers/macro_handler.py index 5bd5c1457..ed6af7103 100644 --- a/inputremapper/injection/mapping_handlers/macro_handler.py +++ b/inputremapper/injection/mapping_handlers/macro_handler.py @@ -31,7 +31,7 @@ HandlerEnums, ) from inputremapper.input_event import InputEvent -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger class MacroHandler(MappingHandler): diff --git a/inputremapper/injection/mapping_handlers/mapping_handler.py b/inputremapper/injection/mapping_handlers/mapping_handler.py index 74b1b6da2..87608a043 100644 --- a/inputremapper/injection/mapping_handlers/mapping_handler.py +++ b/inputremapper/injection/mapping_handlers/mapping_handler.py @@ -69,7 +69,7 @@ from inputremapper.configs.mapping import Mapping from inputremapper.exceptions import MappingParsingError from inputremapper.input_event import InputEvent -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger class EventListener(Protocol): diff --git a/inputremapper/injection/mapping_handlers/mapping_parser.py b/inputremapper/injection/mapping_handlers/mapping_parser.py index d42a816e4..65012e833 100644 --- a/inputremapper/injection/mapping_handlers/mapping_parser.py +++ b/inputremapper/injection/mapping_handlers/mapping_parser.py @@ -52,7 +52,7 @@ from inputremapper.injection.mapping_handlers.rel_to_abs_handler import RelToAbsHandler from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler from inputremapper.injection.mapping_handlers.rel_to_rel_handler import RelToRelHandler -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger from inputremapper.utils import get_evdev_constant_name EventPipelines = Dict[InputConfig, Set[InputEventHandler]] diff --git a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py index d4abc30a8..e8b5ec6a4 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py @@ -47,7 +47,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger class RelToAbsHandler(MappingHandler): diff --git a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py index f8e8b36c5..bbc39d6c8 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py @@ -30,7 +30,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent, EventActions -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger class RelToBtnHandler(MappingHandler): diff --git a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py index e710bbdd2..d41a2c465 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py @@ -45,7 +45,7 @@ InputEventHandler, ) from inputremapper.input_event import InputEvent -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger def is_wheel(event) -> bool: diff --git a/inputremapper/injection/numlock.py b/inputremapper/injection/numlock.py index af9e4a513..2598e24c5 100644 --- a/inputremapper/injection/numlock.py +++ b/inputremapper/injection/numlock.py @@ -28,7 +28,7 @@ import re import subprocess -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger def is_numlock_on(): diff --git a/inputremapper/ipc/pipe.py b/inputremapper/ipc/pipe.py index df4dbab95..755bbac22 100644 --- a/inputremapper/ipc/pipe.py +++ b/inputremapper/ipc/pipe.py @@ -42,7 +42,7 @@ from typing import Optional, AsyncIterator, Union from inputremapper.configs.paths import PathUtils -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger class Pipe: diff --git a/inputremapper/ipc/shared_dict.py b/inputremapper/ipc/shared_dict.py index 4bbb9213c..e2ab4bd84 100644 --- a/inputremapper/ipc/shared_dict.py +++ b/inputremapper/ipc/shared_dict.py @@ -26,7 +26,7 @@ import select from typing import Optional, Any -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger class SharedDict: diff --git a/inputremapper/ipc/socket.py b/inputremapper/ipc/socket.py index c4472c1f4..1b7613963 100644 --- a/inputremapper/ipc/socket.py +++ b/inputremapper/ipc/socket.py @@ -58,7 +58,7 @@ from typing import Union from inputremapper.configs.paths import PathUtils -from inputremapper.logger.logger import logger +from inputremapper.logging.logger import logger # something funny that most likely won't appear in messages. # also add some ones so that 01 in the payload won't offset diff --git a/inputremapper/logger/__init__.py b/inputremapper/logging/__init__.py similarity index 100% rename from inputremapper/logger/__init__.py rename to inputremapper/logging/__init__.py diff --git a/inputremapper/logger/formatter.py b/inputremapper/logging/formatter.py similarity index 100% rename from inputremapper/logger/formatter.py rename to inputremapper/logging/formatter.py diff --git a/inputremapper/logger/logger.py b/inputremapper/logging/logger.py similarity index 98% rename from inputremapper/logger/logger.py rename to inputremapper/logging/logger.py index 2deb3fcfe..abc4f3992 100644 --- a/inputremapper/logger/logger.py +++ b/inputremapper/logging/logger.py @@ -23,7 +23,7 @@ import time from typing import cast -from inputremapper.logger.formatter import ColorfulFormatter +from inputremapper.logging.formatter import ColorfulFormatter try: from inputremapper.commit_hash import COMMIT_HASH diff --git a/tests/lib/logger.py b/tests/lib/logger.py index 9a792acdb..ab7ace952 100644 --- a/tests/lib/logger.py +++ b/tests/lib/logger.py @@ -37,7 +37,7 @@ def update_inputremapper_verbosity(): - from inputremapper.logger.logger import logger + from inputremapper.logging.logger import logger logger.update_verbosity(True) diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index dcaaad157..c89e61bf2 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -28,7 +28,7 @@ import evdev from inputremapper.configs.paths import PathUtils -from inputremapper.logger.logger import ( +from inputremapper.logging.logger import ( logger, ColorfulFormatter, ) diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index b5b622172..27836b19e 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -38,7 +38,7 @@ from inputremapper.configs.migrations import Migrations from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.logger.logger import VERSION +from inputremapper.logging.logger import VERSION from inputremapper.user import UserUtils from tests.lib.test_setup import test_setup from tests.lib.tmp import tmp From 9e567429573e3b68399036b99645cfe1169a6edc Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:59:17 +0200 Subject: [PATCH 25/32] removed unused evdev wrapper --- inputremapper/evdev/__init__.py | 0 inputremapper/evdev/evdev.py | 25 ------------------------- 2 files changed, 25 deletions(-) delete mode 100644 inputremapper/evdev/__init__.py delete mode 100644 inputremapper/evdev/evdev.py diff --git a/inputremapper/evdev/__init__.py b/inputremapper/evdev/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/inputremapper/evdev/evdev.py b/inputremapper/evdev/evdev.py deleted file mode 100644 index 12d948568..000000000 --- a/inputremapper/evdev/evdev.py +++ /dev/null @@ -1,25 +0,0 @@ -import evdev - - -class Evdev: - # A Wrapper for evdev for improved testability, easily allowing all sorts of crazy - # mocks independent of how evdev is imported, and allowing tiny fixes if something - # in evdev goes wrong. Eventually everything should follow the DI pattern, for even - # better testability by allowing to swap the whole Evdev class out with a mock - # class. - - @staticmethod - def list_devices(*args, **kwargs): - return evdev.list_devices(*args, **kwargs) - - @staticmethod - def input_device_factory(*args, **kwargs): - return evdev.InputDevice(*args, **kwargs) - - @staticmethod - def uinput_factory(*args, **kwargs): - return evdev.UInput(*args, **kwargs) - - @staticmethod - def input_event_factory(*args, **kwargs): - return evdev.InputEvent(*args, **kwargs) From 57a7c07de949d1633b3cf7a947c2efad7e0334bb Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:30:29 +0200 Subject: [PATCH 26/32] Injecting GlobalConfig --- bin/input-remapper-control | 9 ++- bin/input-remapper-gtk | 4 +- bin/input-remapper-service | 5 +- inputremapper/configs/global_config.py | 8 +-- inputremapper/daemon.py | 13 +++-- tests/integration/test_daemon.py | 3 +- tests/integration/test_gui.py | 20 +++++-- tests/lib/cleanup.py | 25 ++++----- tests/lib/fixtures.py | 14 ++--- tests/lib/test_setup.py | 15 ----- tests/unit/test_config.py | 10 ++-- tests/unit/test_control.py | 40 ++++++------- tests/unit/test_daemon.py | 78 +++++++++++++------------- tests/unit/test_data_manager.py | 5 +- 14 files changed, 123 insertions(+), 126 deletions(-) diff --git a/bin/input-remapper-control b/bin/input-remapper-control index 499e1262d..81bd49411 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -29,7 +29,7 @@ import sys from enum import Enum from typing import Union, Optional -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.migrations import Migrations from inputremapper.logging.logger import logger @@ -66,6 +66,9 @@ class Options: class InputRemapperControl: + def __init__(self, global_config: GlobalConfig): + self.global_config = global_config + def run(self, cmd) -> None: """Run and log a command.""" logger.info("Running `%s`...", cmd) @@ -131,7 +134,7 @@ class InputRemapperControl: sys.exit(6) logger.info('Using config from "%s" instead', path) - global_config.load_config(path) + self.global_config.load_config(path) def ensure_migrated(self) -> None: # import stuff late to make sure the correct log level is applied @@ -144,7 +147,7 @@ class InputRemapperControl: # This will also refresh the config of the daemon if the user changed # it in the meantime. # config_dir is either the cli arg or the default path in home - config_dir = os.path.dirname(global_config.path) + config_dir = os.path.dirname(self.global_config.path) self.daemon.set_config_dir(config_dir) Migrations.migrate() diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index 40cc25630..f97f0b6d4 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -89,6 +89,8 @@ if __name__ == "__main__": message_broker = MessageBroker() + global_config = GlobalConfig() + # create the reader before we start the reader-service (start_processes) otherwise # it can come to race conditions with the creation of pipes reader_client = ReaderClient(message_broker, _Groups()) @@ -96,7 +98,7 @@ if __name__ == "__main__": data_manager = DataManager( message_broker, - GlobalConfig(), + global_config, reader_client, daemon, GlobalUInputs(), diff --git a/bin/input-remapper-service b/bin/input-remapper-service index 7e3cc2d9e..b76b131ed 100755 --- a/bin/input-remapper-service +++ b/bin/input-remapper-service @@ -25,6 +25,7 @@ import sys from argparse import ArgumentParser +from inputremapper.configs.global_config import GlobalConfig from inputremapper.logging.logger import logger if __name__ == "__main__": @@ -55,6 +56,8 @@ if __name__ == "__main__": if not options.hide_info: logger.log_info("input-remapper-service") - daemon = Daemon() + global_config = GlobalConfig() + + daemon = Daemon(global_config) daemon.publish() daemon.run() diff --git a/inputremapper/configs/global_config.py b/inputremapper/configs/global_config.py index 3c08a5cd0..ee6487f6b 100644 --- a/inputremapper/configs/global_config.py +++ b/inputremapper/configs/global_config.py @@ -25,8 +25,8 @@ from inputremapper.configs.base_config import ConfigBase, INITIAL_CONFIG from inputremapper.configs.paths import PathUtils -from inputremapper.user import UserUtils from inputremapper.logging.logger import logger +from inputremapper.user import UserUtils MOUSE = "mouse" WHEEL = "wheel" @@ -35,7 +35,7 @@ class GlobalConfig(ConfigBase): - """Global default configuration. + """Global default configuration, from which all presets inherit. It can also contain some extra stuff not relevant for presets, like the autoload stuff. If presets have a config key set, it will ignore the default global configuration for that one. If none of the configs @@ -129,7 +129,3 @@ def _save_config(self): json.dump(self._config, file, indent=4) logger.info("Saved config to %s", self.path) file.write("\n") - - -# TODO DI -global_config = GlobalConfig() diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index cf949e9cd..0194c8519 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -41,7 +41,7 @@ from inputremapper.logging.logger import logger from inputremapper.injection.injector import Injector, InjectorState from inputremapper.configs.preset import Preset -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.system_mapping import system_mapping from inputremapper.groups import groups from inputremapper.configs.paths import PathUtils @@ -184,9 +184,12 @@ class Daemon: """ - def __init__(self): + def __init__(self, global_config: GlobalConfig): """Constructs the daemon.""" logger.debug("Creating daemon") + + self.global_config = global_config + self.injectors: Dict[str, Injector] = {} self.config_dir = None @@ -333,7 +336,7 @@ def set_config_dir(self, config_dir: str): return self.config_dir = config_dir - global_config.load_config(config_path) + self.global_config.load_config(config_path) def _autoload(self, group_key: str): """Check if autoloading is a good idea, and if so do it. @@ -351,7 +354,7 @@ def _autoload(self, group_key: str): # either not relevant for input-remapper, or not connected yet return - preset = global_config.get(["autoload", group.key], log_unknown=False) + preset = self.global_config.get(["autoload", group.key], log_unknown=False) if preset is None: # no autoloading is configured for this device @@ -415,7 +418,7 @@ def autoload(self): ) return - autoload_presets = list(global_config.iterate_autoload_presets()) + autoload_presets = list(self.global_config.iterate_autoload_presets()) logger.info("Autoloading for all devices") diff --git a/tests/integration/test_daemon.py b/tests/integration/test_daemon.py index cf8e4e669..27593a24f 100644 --- a/tests/integration/test_daemon.py +++ b/tests/integration/test_daemon.py @@ -40,10 +40,11 @@ def gtk_iteration(): Gtk.main_iteration() -@test_setup @test_setup class TestDBusDaemon(unittest.TestCase): def setUp(self): + # You need to install input-remapper into your system in order for this test + # to work. self.process = multiprocessing.Process( target=os.system, args=("input-remapper-service -d",) ) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index be79a778f..3548bdb74 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -67,7 +67,7 @@ from inputremapper.configs.system_mapping import system_mapping from inputremapper.configs.mapping import Mapping from inputremapper.configs.paths import PathUtils -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.groups import _Groups from inputremapper.gui.data_manager import DataManager from inputremapper.gui.messages.message_broker import ( @@ -102,7 +102,14 @@ def launch( argv=None, -) -> Tuple[UserInterface, Controller, DataManager, MessageBroker, DaemonProxy]: +) -> Tuple[ + UserInterface, + Controller, + DataManager, + MessageBroker, + DaemonProxy, + GlobalConfig, +]: """Start input-remapper-gtk with the command line argument array argv.""" bin_path = os.path.join(get_project_root(), "bin", "input-remapper-gtk") @@ -127,6 +134,7 @@ def launch( module.data_manager, module.message_broker, module.daemon, + module.global_config, ) @@ -161,7 +169,7 @@ def patch_launch(): ), patch.object( Daemon, "connect", - Daemon, + lambda: Daemon(GlobalConfig()), ): yield @@ -218,7 +226,7 @@ def patch_daemon(self): self.daemon_connect_patch = patch.object( Daemon, "connect", - Daemon, + lambda: Daemon(GlobalConfig()), ) self.daemon_connect_patch.start() @@ -233,6 +241,7 @@ def setUp(self): self.data_manager, self.message_broker, self.daemon, + self.global_config, ) = launch() def tearDown(self): @@ -306,6 +315,7 @@ def setUp(self): self.data_manager, self.message_broker, self.daemon, + self.global_config, ) = launch() get = self.user_interface.get @@ -340,7 +350,7 @@ def grab(_): evdev.InputDevice.grab = grab - global_config._save_config() + self.global_config._save_config() self.throttle(20) diff --git a/tests/lib/cleanup.py b/tests/lib/cleanup.py index 14b688a70..ff2cd29df 100644 --- a/tests/lib/cleanup.py +++ b/tests/lib/cleanup.py @@ -20,30 +20,31 @@ from __future__ import annotations +import asyncio import copy import os import shutil import time -import asyncio -import psutil from pickle import UnpicklingError -# TODO on it. You don't need a framework for this by the way: -# don't import anything from input_remapper gloablly here, because some files execute -# code when imported, which can screw up patches. I wish we had a dependency injection -# framework that patches together the dependencies during runtime... +import psutil +from tests.lib.constants import EVENT_READ_TIMEOUT +from tests.lib.fixtures import fixtures from tests.lib.logger import logger +from tests.lib.patches import uinputs from tests.lib.pipes import ( uinput_write_history_pipe, uinput_write_history, pending_events, setup_pipe, ) -from tests.lib.constants import EVENT_READ_TIMEOUT from tests.lib.tmp import tmp -from tests.lib.fixtures import fixtures -from tests.lib.patches import uinputs + +# TODO on it. You don't need a framework for this by the way: +# don't import anything from input_remapper gloablly here, because some files execute +# code when imported, which can screw up patches. I wish we had a dependency injection +# framework that patches together the dependencies during runtime... environ_copy = copy.deepcopy(os.environ) @@ -81,10 +82,8 @@ def quick_cleanup(log=True): # Reminder: before patches are applied in test.py, no inputremapper module # may be imported. So tests.lib imports them just-in-time in functions instead. from inputremapper.injection.macros.macro import macro_variables - from inputremapper.configs.global_config import global_config from inputremapper.configs.system_mapping import system_mapping from inputremapper.gui.utils import debounce_manager - from inputremapper.configs.paths import PathUtils from inputremapper.injection.global_uinputs import global_uinputs from tests.lib.global_uinputs import reset_global_uinputs_for_service @@ -130,10 +129,6 @@ def quick_cleanup(log=True): if os.path.exists(tmp): shutil.rmtree(tmp) - global_config.path = os.path.join(PathUtils.get_config_path(), "config.json") - global_config.clear_config() - global_config._save_config() - system_mapping.populate() clear_write_history() diff --git a/tests/lib/fixtures.py b/tests/lib/fixtures.py index 2cbe9b6c2..4d3614f24 100644 --- a/tests/lib/fixtures.py +++ b/tests/lib/fixtures.py @@ -22,12 +22,16 @@ import dataclasses import json +import time from hashlib import md5 from typing import Dict, Optional -import time import evdev +from inputremapper.configs.input_config import InputCombination +from inputremapper.configs.mapping import Mapping +from inputremapper.configs.paths import PathUtils +from inputremapper.configs.preset import Preset from tests.lib.logger import logger # input-remapper is only interested in devices that have EV_KEY, add some @@ -347,12 +351,6 @@ def prepare_presets(): """prepare a few presets for use in tests "Foo Device 2/preset3" is the newest and "Foo Device 2/preset2" is set to autoload """ - from inputremapper.configs.preset import Preset - from inputremapper.configs.mapping import Mapping - from inputremapper.configs.paths import PathUtils - from inputremapper.configs.global_config import global_config - from inputremapper.configs.input_config import InputCombination - preset1 = Preset(PathUtils.get_preset_path("Foo Device", "preset1")) preset1.add( Mapping.from_combination( @@ -379,6 +377,4 @@ def prepare_presets(): with open(PathUtils.get_config_path("config.json"), "w") as file: json.dump({"autoload": {"Foo Device 2": "preset2"}}, file, indent=4) - global_config.load_config() - return preset1, preset2, preset3 diff --git a/tests/lib/test_setup.py b/tests/lib/test_setup.py index 4961128e1..19b71724b 100644 --- a/tests/lib/test_setup.py +++ b/tests/lib/test_setup.py @@ -23,8 +23,6 @@ import os import tracemalloc -from inputremapper.configs.global_config import global_config -from inputremapper.configs.paths import PathUtils from tests.lib.cleanup import cleanup, quick_cleanup from tests.lib.fixture_pipes import create_fixture_pipes, remove_fixture_pipes from tests.lib.is_service_running import is_service_running @@ -64,14 +62,6 @@ def setUpClass(): for patch in patches: patch.start() - # TODO if global_config is injected instead, it could work without doing this - # load_config call here. Because right now the constructor uses variables - # that are unpatched once global_config.py is imported. - global_config.path = os.path.join( - PathUtils.config_path(), - "config.json", - ) - original_setUpClass() def tearDownClass(): @@ -95,11 +85,6 @@ def tearDown(self): quick_cleanup() - # This assertion was important to me somehow in other tests after the cleanup, - # so I added it to the test setup. I don't remember if this is important to - # check. - assert len(global_config.iterate_autoload_presets()) == 0 - for patch in patches: patch.stop() diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index f80720eeb..317cd3803 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -22,16 +22,16 @@ import os import unittest -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.paths import PathUtils - -from tests.lib.tmp import tmp from tests.lib.test_setup import test_setup +from tests.lib.tmp import tmp @test_setup class TestConfig(unittest.TestCase): def test_basic(self): + global_config = GlobalConfig() self.assertEqual(global_config.get("a"), None) global_config.set("a", 1) @@ -48,6 +48,7 @@ def test_basic(self): self.assertEqual(global_config._config["a"]["b"]["c"], 3) def test_autoload(self): + global_config = GlobalConfig() self.assertEqual(len(global_config.iterate_autoload_presets()), 0) self.assertFalse(global_config.is_autoloaded("d1", "a")) self.assertFalse(global_config.is_autoloaded("d2.foo", "b")) @@ -92,9 +93,9 @@ def test_autoload(self): self.assertRaises(ValueError, global_config.is_autoloaded, None, "a") def test_initial(self): + global_config = GlobalConfig() # when loading for the first time, create a config file with # the default values - os.remove(global_config.path) self.assertFalse(os.path.exists(global_config.path)) global_config.load_config() self.assertTrue(os.path.exists(global_config.path)) @@ -104,6 +105,7 @@ def test_initial(self): self.assertIn('"autoload": {}', contents) def test_save_load(self): + global_config = GlobalConfig() self.assertEqual(len(global_config.iterate_autoload_presets()), 0) global_config.load_config() diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index f2d800385..8c43c7608 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -28,7 +28,7 @@ from importlib.util import spec_from_loader, module_from_spec from unittest.mock import patch -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.daemon import Daemon @@ -40,7 +40,8 @@ def import_control(): """Import the core function of the input-remapper-control command.""" bin_path = os.path.join( - os.getcwd().replace("/tests", ""), + os.getcwd().rsplit("input-remapper")[0], + "input-remapper", "bin", "input-remapper-control", ) @@ -65,7 +66,8 @@ def import_control(): @test_setup class TestControl(unittest.TestCase): def setUp(self): - self.input_remapper_control = InputRemapperControl() + self.global_config = GlobalConfig() + self.input_remapper_control = InputRemapperControl(self.global_config) def test_autoload(self): device_keys = ["Foo Device 2", "Bar Device"] @@ -81,7 +83,7 @@ def test_autoload(self): Preset(paths[1]).save() Preset(paths[2]).save() - daemon = Daemon() + daemon = Daemon(self.global_config) self.input_remapper_control.set_daemon(daemon) @@ -101,8 +103,8 @@ def start_injecting(device: str, preset: str): patch.object(daemon, "start_injecting", start_injecting).start() - global_config.set_autoload_preset(groups_[0].key, presets[0]) - global_config.set_autoload_preset(groups_[1].key, presets[1]) + self.global_config.set_autoload_preset(groups_[0].key, presets[0]) + self.global_config.set_autoload_preset(groups_[1].key, presets[1]) self.input_remapper_control.communicate( command="autoload", @@ -181,7 +183,7 @@ def start_injecting(device: str, preset: str): daemon.autoload_history.may_autoload(groups_[1].key, presets[1]) ) self.assertEqual(stop_counter, 3) - global_config.set_autoload_preset(groups_[1].key, presets[2]) + self.global_config.set_autoload_preset(groups_[1].key, presets[2]) self.input_remapper_control.communicate( command="autoload", config_dir=None, @@ -236,16 +238,16 @@ def test_autoload_other_path(self): Preset(paths[0]).save() Preset(paths[1]).save() - daemon = Daemon() + daemon = Daemon(self.global_config) self.input_remapper_control.set_daemon(daemon) start_history = [] daemon.start_injecting = lambda *args: start_history.append(args) - global_config.path = os.path.join(config_dir, "config.json") - global_config.load_config() - global_config.set_autoload_preset(device_names[0], presets[0]) - global_config.set_autoload_preset(device_names[1], presets[1]) + self.global_config.path = os.path.join(config_dir, "config.json") + self.global_config.load_config() + self.global_config.set_autoload_preset(device_names[0], presets[0]) + self.global_config.set_autoload_preset(device_names[1], presets[1]) self.input_remapper_control.communicate( command="autoload", @@ -262,7 +264,7 @@ def test_start_stop(self): group = groups.find(key="Foo Device 2") preset = "preset9" - daemon = Daemon() + daemon = Daemon(self.global_config) self.input_remapper_control.set_daemon(daemon) start_history = [] @@ -306,7 +308,7 @@ def test_config_not_found(self): path = "~/a/preset.json" config_dir = "/foo/bar" - daemon = Daemon() + daemon = Daemon(self.global_config) self.input_remapper_control.set_daemon(daemon) start_history = [] @@ -335,26 +337,26 @@ def test_config_not_found(self): ) def test_autoload_config_dir(self): - daemon = Daemon() + daemon = Daemon(self.global_config) path = os.path.join(tmp, "foo") os.makedirs(path) with open(os.path.join(path, "config.json"), "w") as file: file.write('{"foo":"bar"}') - self.assertIsNone(global_config.get("foo")) + self.assertIsNone(self.global_config.get("foo")) daemon.set_config_dir(path) # since daemon and this test share the same memory, the global_config # object that this test can access will be modified - self.assertEqual(global_config.get("foo"), "bar") + self.assertEqual(self.global_config.get("foo"), "bar") # passing a path that doesn't exist or a path that doesn't contain # a config.json file won't do anything os.makedirs(os.path.join(tmp, "bar")) daemon.set_config_dir(os.path.join(tmp, "bar")) - self.assertEqual(global_config.get("foo"), "bar") + self.assertEqual(self.global_config.get("foo"), "bar") daemon.set_config_dir(os.path.join(tmp, "qux")) - self.assertEqual(global_config.get("foo"), "bar") + self.assertEqual(self.global_config.get("foo"), "bar") def test_internals_reader(self): with patch.object(os, "system") as os_system_patch: diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index e6f01314a..327a81f41 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -18,38 +18,35 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from unittest.mock import patch, MagicMock - -from evdev._ecodes import EV_ABS - -from inputremapper.input_event import InputEvent -from tests.lib.logger import logger -from tests.lib.cleanup import cleanup -from tests.lib.fixtures import Fixture -from tests.lib.pipes import push_events, uinput_write_history_pipe -from tests.lib.tmp import tmp -from tests.lib.fixtures import fixtures - +import json import os -import unittest import time -import json +import unittest +from unittest.mock import patch, MagicMock import evdev +from evdev._ecodes import EV_ABS from evdev.ecodes import EV_KEY, KEY_B, KEY_A, ABS_X, BTN_A, BTN_B from pydbus import SystemBus -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.global_config import GlobalConfig +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping -from inputremapper.configs.global_config import global_config -from inputremapper.groups import groups from inputremapper.configs.paths import PathUtils -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.preset import Preset -from inputremapper.injection.injector import InjectorState +from inputremapper.configs.system_mapping import system_mapping from inputremapper.daemon import Daemon +from inputremapper.groups import groups from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.injector import InjectorState +from inputremapper.input_event import InputEvent +from tests.lib.cleanup import cleanup +from tests.lib.fixtures import Fixture +from tests.lib.fixtures import fixtures +from tests.lib.logger import logger +from tests.lib.pipes import push_events, uinput_write_history_pipe from tests.lib.test_setup import test_setup, is_service_running +from tests.lib.tmp import tmp @test_setup @@ -58,8 +55,9 @@ class TestDaemon(unittest.TestCase): def setUp(self): self.daemon = None + self.global_config = GlobalConfig() PathUtils.mkdir(PathUtils.get_config_path()) - global_config._save_config() + self.global_config._save_config() # the daemon should be able to create them on demand: global_uinputs.devices = {} @@ -134,7 +132,7 @@ def test_daemon(self): ) ) preset.save() - global_config.set_autoload_preset(group.key, preset_name) + self.global_config.set_autoload_preset(group.key, preset_name) """Injection 1""" @@ -144,7 +142,7 @@ def test_daemon(self): [InputEvent.key(BTN_B, 1, fixtures.gamepad.get_device_hash())], ) - self.daemon = Daemon() + self.daemon = Daemon(self.global_config) self.assertFalse(uinput_write_history_pipe[0].poll()) @@ -202,15 +200,15 @@ def test_daemon(self): self.assertEqual(event.value, 1) def test_config_dir(self): - global_config.set("foo", "bar") - self.assertEqual(global_config.get("foo"), "bar") + self.global_config.set("foo", "bar") + self.assertEqual(self.global_config.get("foo"), "bar") # freshly loads the config and therefore removes the previosly added key. # This is important so that if the service is started via sudo or pkexec # it knows where to look for configuration files. - self.daemon = Daemon() + self.daemon = Daemon(self.global_config) self.assertEqual(self.daemon.config_dir, PathUtils.get_config_path()) - self.assertIsNone(global_config.get("foo")) + self.assertIsNone(self.global_config.get("foo")) def test_refresh_on_start(self): if os.path.exists(PathUtils.get_config_path("xmodmap.json")): @@ -245,8 +243,8 @@ def test_refresh_on_start(self): system_mapping.clear() preset.save() - global_config.set_autoload_preset(group_key, preset_name) - self.daemon = Daemon() + self.global_config.set_autoload_preset(group_key, preset_name) + self.daemon = Daemon(self.global_config) # make sure the devices are populated groups.refresh() @@ -283,7 +281,7 @@ def test_refresh_for_unknown_key(self): # this test only makes sense if this device is unknown yet self.assertIsNone(groups.find(name=device_9876)) - self.daemon = Daemon() + self.daemon = Daemon(self.global_config) # make sure the devices are populated groups.refresh() @@ -345,8 +343,8 @@ def test_xmodmap_file(self): # an existing config file is needed otherwise set_config_dir refuses # to use the directory config_path = os.path.join(config_dir, "config.json") - global_config.path = config_path - global_config._save_config() + self.global_config.path = config_path + self.global_config._save_config() # finally, create the xmodmap file xmodmap_path = os.path.join(config_dir, "xmodmap.json") @@ -355,7 +353,7 @@ def test_xmodmap_file(self): # test setup complete - self.daemon = Daemon() + self.daemon = Daemon(self.global_config) self.daemon.set_config_dir(config_dir) self.daemon.start_injecting(group.key, preset_name) @@ -373,7 +371,7 @@ def test_start_stop(self): group = groups.find(key=group_key) preset_name = "preset8" - daemon = Daemon() + daemon = Daemon(self.global_config) self.daemon = daemon pereset = Preset(group.get_preset_path(preset_name)) @@ -441,7 +439,7 @@ def test_autoload(self): group_key = "Qux/Device?" group = groups.find(key=group_key) - daemon = Daemon() + daemon = Daemon(self.global_config) self.daemon = daemon preset = Preset(group.get_preset_path(preset_name)) @@ -459,7 +457,7 @@ def test_autoload(self): self.assertNotIn(group_key, daemon.autoload_history._autoload_history) self.assertTrue(daemon.autoload_history.may_autoload(group_key, preset_name)) - global_config.set_autoload_preset(group_key, preset_name) + self.global_config.set_autoload_preset(group_key, preset_name) len_before = len(self.daemon.autoload_history._autoload_history) # now autoloading is configured, so it will autoload self.daemon._autoload(group_key) @@ -498,7 +496,7 @@ def test_autoload(self): self.assertEqual(len_before, len_after) def test_autoload_2(self): - self.daemon = Daemon() + self.daemon = Daemon(self.global_config) history = self.daemon.autoload_history._autoload_history # existing device @@ -513,10 +511,10 @@ def test_autoload_2(self): ) ) preset.save() - global_config.set_autoload_preset(group.key, preset_name) + self.global_config.set_autoload_preset(group.key, preset_name) # ignored, won't cause problems: - global_config.set_autoload_preset("non-existant-key", "foo") + self.global_config.set_autoload_preset("non-existant-key", "foo") self.daemon.autoload() self.assertEqual(len(history), 1) @@ -537,9 +535,9 @@ def test_autoload_3(self): ) preset.save() - global_config.set_autoload_preset(group.key, preset_name) + self.global_config.set_autoload_preset(group.key, preset_name) - self.daemon = Daemon() + self.daemon = Daemon(self.global_config) groups.set_groups([]) # caused the bug self.assertIsNone(groups.find(key="Foo Device 2")) self.daemon.autoload() diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index 18c5873b3..a1960736a 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -25,7 +25,7 @@ from typing import List from unittest.mock import MagicMock, call -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import UIMapping, MappingData from inputremapper.configs.paths import PathUtils @@ -64,9 +64,10 @@ def setUp(self) -> None: self.reader = ReaderClient(self.message_broker, _Groups()) self.uinputs = GlobalUInputs() self.uinputs.prepare_all() + self.global_config = GlobalConfig() self.data_manager = DataManager( self.message_broker, - global_config, + self.global_config, self.reader, FakeDaemonProxy(), self.uinputs, From e0a8127c4f1821940630eedfb270595badcca186 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:46:26 +0200 Subject: [PATCH 27/32] more refactoring --- bin/input-remapper-control | 19 +- bin/input-remapper-gtk | 9 +- bin/input-remapper-reader-service | 4 +- bin/input-remapper-service | 6 +- inputremapper/configs/migrations.py | 98 ++-- inputremapper/daemon.py | 17 +- inputremapper/gui/reader_service.py | 26 +- inputremapper/injection/context.py | 5 +- inputremapper/injection/global_uinputs.py | 35 +- inputremapper/injection/injector.py | 16 +- .../mapping_handlers/abs_to_abs_handler.py | 7 +- .../mapping_handlers/abs_to_btn_handler.py | 4 +- .../mapping_handlers/abs_to_rel_handler.py | 9 +- .../mapping_handlers/axis_switch_handler.py | 4 +- .../mapping_handlers/combination_handler.py | 4 +- .../mapping_handlers/hierarchy_handler.py | 8 +- .../injection/mapping_handlers/key_handler.py | 9 +- .../mapping_handlers/macro_handler.py | 9 +- .../mapping_handlers/mapping_handler.py | 3 + .../mapping_handlers/mapping_parser.py | 486 ++++++++++-------- .../mapping_handlers/rel_to_abs_handler.py | 7 +- .../mapping_handlers/rel_to_btn_handler.py | 4 +- .../mapping_handlers/rel_to_rel_handler.py | 7 +- tests/integration/test_gui.py | 48 +- tests/lib/cleanup.py | 8 +- tests/lib/global_uinputs.py | 35 -- tests/unit/test_context.py | 7 +- tests/unit/test_control.py | 20 +- tests/unit/test_controller.py | 4 +- tests/unit/test_daemon.py | 65 ++- tests/unit/test_data_manager.py | 4 +- .../test_event_pipeline.py | 97 ++-- .../test_mapping_handlers.py | 51 +- tests/unit/test_event_reader.py | 15 +- tests/unit/test_global_uinputs.py | 20 +- tests/unit/test_injector.py | 55 +- tests/unit/test_macros.py | 11 +- tests/unit/test_migrations.py | 39 +- tests/unit/test_reader.py | 12 +- tests/unit/test_test.py | 8 +- 40 files changed, 756 insertions(+), 539 deletions(-) delete mode 100644 tests/lib/global_uinputs.py diff --git a/bin/input-remapper-control b/bin/input-remapper-control index 81bd49411..33659904a 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -31,6 +31,7 @@ from typing import Union, Optional from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.migrations import Migrations +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput, FrontendUInput from inputremapper.logging.logger import logger @@ -66,8 +67,13 @@ class Options: class InputRemapperControl: - def __init__(self, global_config: GlobalConfig): + def __init__( + self, + global_config: GlobalConfig, + migrations: Migrations, + ): self.global_config = global_config + self.migrations = migrations def run(self, cmd) -> None: """Run and log a command.""" @@ -149,7 +155,7 @@ class InputRemapperControl: # config_dir is either the cli arg or the default path in home config_dir = os.path.dirname(self.global_config.path) self.daemon.set_config_dir(config_dir) - Migrations.migrate() + self.migrations.migrate() def _stop(self, device: str) -> None: group = self._require_group(device) @@ -270,7 +276,14 @@ class InputRemapperControl: def main(options: Options) -> None: - input_remapper_control = InputRemapperControl() + global_config = GlobalConfig() + global_uinputs = GlobalUInputs(FrontendUInput) + migrations = Migrations(global_uinputs) + input_remapper_control = InputRemapperControl( + global_config, + migrations, + ) + if options.debug: logger.update_verbosity(True) diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index f97f0b6d4..77a30b4d3 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -78,14 +78,17 @@ if __name__ == "__main__": from inputremapper.gui.data_manager import DataManager from inputremapper.gui.user_interface import UserInterface from inputremapper.gui.controller import Controller - from inputremapper.injection.global_uinputs import GlobalUInputs + from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput from inputremapper.groups import _Groups from inputremapper.gui.reader_client import ReaderClient from inputremapper.daemon import Daemon from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.migrations import Migrations - Migrations.migrate() + global_uinputs = GlobalUInputs(FrontendUInput) + migrations = Migrations(global_uinputs) + + Migrations(global_uinputs).migrate() message_broker = MessageBroker() @@ -101,7 +104,7 @@ if __name__ == "__main__": global_config, reader_client, daemon, - GlobalUInputs(), + global_uinputs, system_mapping, ) controller = Controller(message_broker, data_manager) diff --git a/bin/input-remapper-reader-service b/bin/input-remapper-reader-service index e564b0426..dde437b4c 100755 --- a/bin/input-remapper-reader-service +++ b/bin/input-remapper-reader-service @@ -28,6 +28,7 @@ import sys from argparse import ArgumentParser from inputremapper.groups import _Groups +from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput from inputremapper.logging.logger import logger if __name__ == "__main__": @@ -57,5 +58,6 @@ if __name__ == "__main__": atexit.register(on_exit) groups = _Groups() - reader_service = ReaderService(groups) + global_uinputs = GlobalUInputs(FrontendUInput) + reader_service = ReaderService(groups, global_uinputs) asyncio.run(reader_service.run()) diff --git a/bin/input-remapper-service b/bin/input-remapper-service index b76b131ed..ad7ccff71 100755 --- a/bin/input-remapper-service +++ b/bin/input-remapper-service @@ -26,6 +26,8 @@ import sys from argparse import ArgumentParser from inputremapper.configs.global_config import GlobalConfig +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.logging.logger import logger if __name__ == "__main__": @@ -57,7 +59,9 @@ if __name__ == "__main__": logger.log_info("input-remapper-service") global_config = GlobalConfig() + global_uinputs = GlobalUInputs(UInput) + mapping_parser = MappingParser(global_uinputs) - daemon = Daemon(global_config) + daemon = Daemon(global_config, global_uinputs, mapping_parser) daemon.publish() daemon.run() diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index 877efe75a..2131a1826 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -52,7 +52,7 @@ from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.configs.system_mapping import system_mapping -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.macros.parse import is_this_a_macro from inputremapper.logging.logger import logger, VERSION from inputremapper.user import UserUtils @@ -66,43 +66,46 @@ class Config(TypedDict): class Migrations: - @staticmethod - def migrate(): + def __init__(self, global_uinputs: GlobalUInputs): + self.global_uinputs = global_uinputs + + def migrate(self): """Migrate config files to the current release.""" - Migrations._rename_to_input_remapper() + self._rename_to_input_remapper() - Migrations._copy_to_v2() + self._copy_to_v2() - v = Migrations.config_version() + v = self.config_version() if v < pkg_resources.parse_version("0.4.0"): - Migrations._config_suffix() - Migrations._preset_path() + self._config_suffix() + self._preset_path() if v < pkg_resources.parse_version("1.2.2"): - Migrations._mapping_keys() + self._mapping_keys() if v < pkg_resources.parse_version("1.4.0"): - global_uinputs.prepare_all() - Migrations._add_target() + self.global_uinputs.prepare_all() + self._add_target() if v < pkg_resources.parse_version("1.4.1"): - Migrations._otherwise_to_else() + self._otherwise_to_else() if v < pkg_resources.parse_version("1.5.0"): - Migrations._remove_logs() + self._remove_logs() if v < pkg_resources.parse_version("1.6.0-beta"): - Migrations._convert_to_individual_mappings() + self._convert_to_individual_mappings() # add new migrations here if v < pkg_resources.parse_version(VERSION): - Migrations._update_version() + self._update_version() - @staticmethod - def all_presets() -> Iterator[Tuple[os.PathLike, Dict | List]]: + def all_presets( + self, + ) -> Iterator[Tuple[os.PathLike, Dict | List]]: """Get all presets for all groups as list.""" if not os.path.exists(PathUtils.get_preset_path()): return @@ -124,8 +127,7 @@ def all_presets() -> Iterator[Tuple[os.PathLike, Dict | List]]: logger.warning('Invalid json format in preset "%s"', preset) continue - @staticmethod - def config_version(): + def config_version(self): """Get the version string in config.json as packaging.Version object.""" config_path = os.path.join(PathUtils.config_path(), "config.json") @@ -140,8 +142,7 @@ def config_version(): return pkg_resources.parse_version("0.0.0") - @staticmethod - def _config_suffix(): + def _config_suffix(self): """Append the .json suffix to the config file.""" deprecated_path = os.path.join(PathUtils.config_path(), "config") config_path = os.path.join(PathUtils.config_path(), "config.json") @@ -149,8 +150,7 @@ def _config_suffix(): logger.info('Moving "%s" to "%s"', deprecated_path, config_path) os.rename(deprecated_path, config_path) - @staticmethod - def _preset_path(): + def _preset_path(self): """Migrate the folder structure from < 0.4.0. Move existing presets into the new subfolder 'presets' @@ -173,13 +173,12 @@ def _preset_path(): logger.info("done") - @staticmethod - def _mapping_keys(): + def _mapping_keys(self): """Update all preset mappings. Update all keys in preset to include value e.g.: '1,5'->'1,5,1' """ - for preset, preset_structure in Migrations.all_presets(): + for preset, preset_structure in self.all_presets(): if isinstance(preset_structure, list): continue # the preset must be at least 1.6-beta version @@ -199,8 +198,7 @@ def _mapping_keys(): json.dump(preset_structure, file, indent=4) file.write("\n") - @staticmethod - def _update_version(): + def _update_version(self): """Write the current version to the config file.""" config_file = os.path.join(PathUtils.config_path(), "config.json") if not os.path.exists(config_file): @@ -214,8 +212,7 @@ def _update_version(): logger.info('Updating version in config to "%s"', VERSION) json.dump(config, file, indent=4) - @staticmethod - def _rename_to_input_remapper(): + def _rename_to_input_remapper(self): """Rename .config/key-mapper to .config/input-remapper.""" old_config_path = os.path.join(UserUtils.home, ".config/key-mapper") if not os.path.exists(PathUtils.config_path()) and os.path.exists( @@ -224,8 +221,7 @@ def _rename_to_input_remapper(): logger.info("Moving %s to %s", old_config_path, PathUtils.config_path()) shutil.move(old_config_path, PathUtils.config_path()) - @staticmethod - def _find_target(symbol): + def _find_target(self, symbol): """Try to find a uinput with the required capabilities for the symbol.""" capabilities = {EV_KEY: set(), EV_REL: set()} @@ -239,17 +235,16 @@ def _find_target(symbol): if len(capabilities[EV_REL]) > 0: return "mouse" - for name, uinput in global_uinputs.devices.items(): + for name, uinput in self.global_uinputs.devices.items(): if capabilities[EV_KEY].issubset(uinput.capabilities()[EV_KEY]): return name logger.info('could not find a suitable target UInput for "%s"', symbol) return None - @staticmethod - def _add_target(): + def _add_target(self): """Add the target field to each preset mapping.""" - for preset, preset_structure in Migrations.all_presets(): + for preset, preset_structure in self.all_presets(): if isinstance(preset_structure, list): continue @@ -261,7 +256,7 @@ def _add_target(): if isinstance(symbol, list): continue - target = Migrations._find_target(symbol) + target = self._find_target(symbol) if target is None: target = "keyboard" symbol = ( @@ -288,10 +283,9 @@ def _add_target(): json.dump(preset_structure, file, indent=4) file.write("\n") - @staticmethod - def _otherwise_to_else(): + def _otherwise_to_else(self): """Conditional macros should use an "else" parameter instead of "otherwise".""" - for preset, preset_structure in Migrations.all_presets(): + for preset, preset_structure in self.all_presets(): if isinstance(preset_structure, list): continue @@ -328,8 +322,9 @@ def _otherwise_to_else(): json.dump(preset_structure, file, indent=4) file.write("\n") - @staticmethod - def _input_combination_from_string(combination_string: str) -> InputCombination: + def _input_combination_from_string( + self, combination_string: str + ) -> InputCombination: configs = [] for event_str in combination_string.split("+"): type_, code, analog_threshold = event_str.split(",") @@ -343,14 +338,15 @@ def _input_combination_from_string(combination_string: str) -> InputCombination: return InputCombination(configs) - @staticmethod - def _convert_to_individual_mappings() -> None: + def _convert_to_individual_mappings( + self, + ) -> None: """Convert preset.json from {key: [symbol, target]} to [{input_combination: ..., output_symbol: symbol, ...}] """ - for old_preset_path, old_preset in Migrations.all_presets(): + for old_preset_path, old_preset in self.all_presets(): if isinstance(old_preset, list): continue @@ -363,9 +359,7 @@ def _convert_to_individual_mappings() -> None: symbol_target, ) try: - combination = Migrations._input_combination_from_string( - combination - ) + combination = self._input_combination_from_string(combination) except ValueError: logger.error( "unable to migrate mapping with invalid combination %s", @@ -482,8 +476,7 @@ def _convert_to_individual_mappings() -> None: migrated_preset.save() - @staticmethod - def _copy_to_v2(): + def _copy_to_v2(self): """Move the beta config to the v2 path, or copy the v1 config to the v2 path.""" # TODO test if os.path.exists(PathUtils.config_path()): @@ -495,7 +488,7 @@ def _copy_to_v2(): old_path = os.path.join(UserUtils.home, ".config/input-remapper") if os.path.exists(os.path.join(old_path, "config.json")): # no beta path, only old presets exist. COPY to v2 path, which will then be - # migrated by the various migrations. + # migrated by the various self. logger.debug("copying all from %s to %s", old_path, PathUtils.config_path()) shutil.copytree(old_path, PathUtils.config_path()) return @@ -511,8 +504,7 @@ def _copy_to_v2(): logger.debug("moving %s to %s", beta_path, PathUtils.config_path()) shutil.move(beta_path, PathUtils.config_path()) - @staticmethod - def _remove_logs(): + def _remove_logs(self): """We will try to rely on journalctl for this in the future.""" try: PathUtils.remove(f"{UserUtils.home}/.log/input-remapper") diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index 0194c8519..b088cb2f8 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -35,6 +35,8 @@ import gi from pydbus import SystemBus +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser + gi.require_version("GLib", "2.0") from gi.repository import GLib @@ -47,7 +49,7 @@ from inputremapper.configs.paths import PathUtils from inputremapper.user import UserUtils from inputremapper.injection.macros.macro import macro_variables -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs BUS_NAME = "inputremapper.Control" @@ -184,11 +186,18 @@ class Daemon: """ - def __init__(self, global_config: GlobalConfig): + def __init__( + self, + global_config: GlobalConfig, + global_uinputs: GlobalUInputs, + mapping_parser: MappingParser, + ): """Constructs the daemon.""" logger.debug("Creating daemon") self.global_config = global_config + self.global_uinputs = global_uinputs + self.mapping_parser = mapping_parser self.injectors: Dict[str, Injector] = {} @@ -503,13 +512,13 @@ def start_injecting(self, group_key: str, preset_name: str) -> bool: # confusing the system. Seems to be especially important with # gamepads, because some apps treat the first gamepad they found # as the only gamepad they'll ever care about. - global_uinputs.prepare_single(mapping.target_uinput) + self.global_uinputs.prepare_single(mapping.target_uinput) if self.injectors.get(group_key) is not None: self.stop_injecting(group_key) try: - injector = Injector(group, preset) + injector = Injector(group, preset, self.mapping_parser) injector.start() self.injectors[group.key] = injector except OSError: diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index b0180e38d..3f1f1d9a4 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -55,6 +55,7 @@ from inputremapper.configs.mapping import Mapping from inputremapper.groups import _Groups, _Group from inputremapper.injection.event_reader import EventReader +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler from inputremapper.injection.mapping_handlers.mapping_handler import ( NotifyCallback, @@ -113,10 +114,11 @@ class ReaderService: _maximum_lifetime: int = 60 * 15 _timeout_tolerance: int = 60 - def __init__(self, groups: _Groups): + def __init__(self, groups: _Groups, global_uinputs: GlobalUInputs): """Construct the reader-service and initialize its communication pipes.""" self._start_time = time.time() self.groups = groups + self.global_uinputs = global_uinputs self._results_pipe = Pipe(get_pipe_paths()[0]) self._commands_pipe = Pipe(get_pipe_paths()[1]) self._pipe = multiprocessing.Pipe() @@ -291,7 +293,9 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum output_symbol="KEY_A", ) handler: MappingHandler = AbsToBtnHandler( - InputCombination([input_config]), mapping + InputCombination([input_config]), + mapping, + self.global_uinputs, ) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) context_dummy.add_handler(input_config, handler) @@ -303,7 +307,11 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum target_uinput="keyboard", output_symbol="KEY_A", ) - handler = AbsToBtnHandler(InputCombination([input_config]), mapping) + handler = AbsToBtnHandler( + InputCombination([input_config]), + mapping, + self.global_uinputs, + ) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) context_dummy.add_handler(input_config, handler) @@ -322,7 +330,11 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum release_timeout=0.3, force_release_timeout=True, ) - handler = RelToBtnHandler(InputCombination([input_config]), mapping) + handler = RelToBtnHandler( + InputCombination([input_config]), + mapping, + self.global_uinputs, + ) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) context_dummy.add_handler(input_config, handler) @@ -337,7 +349,11 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum release_timeout=0.3, force_release_timeout=True, ) - handler = RelToBtnHandler(InputCombination([input_config]), mapping) + handler = RelToBtnHandler( + InputCombination([input_config]), + mapping, + self.global_uinputs, + ) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) context_dummy.add_handler(input_config, handler) diff --git a/inputremapper/injection/context.py b/inputremapper/injection/context.py index cf56608b1..ce73b3f4f 100644 --- a/inputremapper/injection/context.py +++ b/inputremapper/injection/context.py @@ -34,7 +34,7 @@ NotifyCallback, ) from inputremapper.injection.mapping_handlers.mapping_parser import ( - parse_mappings, + MappingParser, EventPipelines, ) from inputremapper.input_event import InputEvent @@ -82,6 +82,7 @@ def __init__( preset: Preset, source_devices: Dict[DeviceHash, evdev.InputDevice], forward_devices: Dict[DeviceHash, evdev.UInput], + mapping_parser: MappingParser, ): if len(forward_devices) == 0: logger.warning("Not forward_devices set") @@ -93,7 +94,7 @@ def __init__( self._source_devices = source_devices self._forward_devices = forward_devices self._notify_callbacks = defaultdict(list) - self._handlers = parse_mappings(preset, self) + self._handlers = mapping_parser.parse_mappings(preset, self) self._create_callbacks() diff --git a/inputremapper/injection/global_uinputs.py b/inputremapper/injection/global_uinputs.py index 3fba120c7..6802db5aa 100644 --- a/inputremapper/injection/global_uinputs.py +++ b/inputremapper/injection/global_uinputs.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from typing import Dict, Union, Tuple, Optional, List +from typing import Dict, Union, Tuple, Optional, List, Type import evdev @@ -83,7 +83,7 @@ def can_emit(self, event: Tuple[int, int, int]): class FrontendUInput: """Uinput which can not actually send events, for use in the frontend.""" - def __init__(self, *args, events=None, name="py-evdev-uinput", **kwargs): + def __init__(self, *_, events=None, name="py-evdev-uinput", **__): # see https://python-evdev.readthedocs.io/en/latest/apidoc.html#module-evdev.uinput # noqa pylint: disable=line-too-long self.events = events self.name = name @@ -97,10 +97,12 @@ def capabilities(self): class GlobalUInputs: """Manages all UInputs that are shared between all injection processes.""" - def __init__(self): + def __init__( + self, + uinput_factory: Union[Type[UInput], Type[FrontendUInput]], + ): self.devices: Dict[str, Union[UInput, FrontendUInput]] = {} - self._uinput_factory = None - self.is_service = inputremapper.utils.is_service() + self._uinput_factory = uinput_factory def __iter__(self): return iter(uinput for _, uinput in self.devices.items()) @@ -121,28 +123,11 @@ def find_fitting_default_uinputs(type_: int, code: int) -> List[str]: ] def reset(self): - self.is_service = inputremapper.utils.is_service() - self._uinput_factory = None self.devices = {} self.prepare_all() - def ensure_uinput_factory_set(self): - if self._uinput_factory is not None: - return - - # TODO this should be solved via DI - # overwrite global_uinputs.is_service in tests to control this - if self.is_service: - logger.debug("Creating regular UInputs") - self._uinput_factory = UInput - else: - logger.debug("Creating FrontendUInputs") - self._uinput_factory = FrontendUInput - def prepare_all(self): """Generate UInputs.""" - self.ensure_uinput_factory_set() - for name, events in DEFAULT_UINPUTS.items(): if name in self.devices.keys(): continue @@ -158,8 +143,6 @@ def prepare_single(self, name: str): This has to be done in the main process before injections that use it start. """ - self.ensure_uinput_factory_set() - if name not in DEFAULT_UINPUTS: raise KeyError("Could not find a matching uinput to generate.") @@ -204,7 +187,3 @@ def get_uinput(self, name: str) -> Optional[evdev.UInput]: return None return self.devices.get(name) - - -# TODO DI -global_uinputs = GlobalUInputs() diff --git a/inputremapper/injection/injector.py b/inputremapper/injection/injector.py index 35db52f49..2636765d9 100644 --- a/inputremapper/injection/injector.py +++ b/inputremapper/injection/injector.py @@ -43,6 +43,7 @@ from inputremapper.gui.messages.message_broker import MessageType from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.injection.numlock import set_numlock, is_numlock_on, ensure_numlock from inputremapper.logging.logger import logger from inputremapper.utils import get_device_hash @@ -119,7 +120,12 @@ class Injector(multiprocessing.Process): regrab_timeout = 0.2 - def __init__(self, group: _Group, preset: Preset) -> None: + def __init__( + self, + group: _Group, + preset: Preset, + mapping_parser: MappingParser, + ) -> None: """ Parameters @@ -128,6 +134,7 @@ def __init__(self, group: _Group, preset: Preset) -> None: the device group """ self.group = group + self.mapping_parser = mapping_parser self._state = InjectorState.UNKNOWN # used to interact with the parts of this class that are running within @@ -415,7 +422,12 @@ def run(self) -> None: # create this within the process after the event loop creation, # so that the macros use the correct loop - self.context = Context(self.preset, sources, forward_devices) + self.context = Context( + self.preset, + sources, + forward_devices, + self.mapping_parser, + ) self._stop_event = asyncio.Event() if len(sources) == 0: diff --git a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py index 2159896fe..0f06a1e18 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py @@ -25,7 +25,7 @@ from inputremapper import exceptions from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, @@ -49,9 +49,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) # find the input event we are supposed to map. If the input combination is # BTN_A + ABS_X + BTN_B, then use the value of ABS_X for the transformation @@ -133,7 +134,7 @@ def _scale_to_target(self, x: float) -> int: def _write(self, value: int): """Inject.""" try: - global_uinputs.write( + self.global_uinputs.write( (*self._output_axis, value), self.mapping.target_uinput ) except OverflowError: diff --git a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py index 544786fde..e173d7e91 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py @@ -24,6 +24,7 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, InputEventHandler, @@ -43,9 +44,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ): - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) self._active = False self._input_config = combination[0] diff --git a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py index fe323b1a0..ebde56417 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py @@ -41,7 +41,7 @@ WHEEL_HI_RES_SCALING, DEFAULT_REL_RATE, ) -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, @@ -134,9 +134,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) # find the input event we are supposed to map assert (map_axis := combination.find_analog_input_config(type_=EV_ABS)) @@ -228,7 +229,9 @@ def _write(self, type_, keycode, value): return # rel 0 does not make sense try: - global_uinputs.write((type_, keycode, value), self.mapping.target_uinput) + self.global_uinputs.write( + (type_, keycode, value), self.mapping.target_uinput + ) except OverflowError: # screwed up the calculation of mouse movements logger.error("OverflowError (%s, %s, %s)", type_, keycode, value) diff --git a/inputremapper/injection/mapping_handlers/axis_switch_handler.py b/inputremapper/injection/mapping_handlers/axis_switch_handler.py index f96b1fc0e..2f86ef178 100644 --- a/inputremapper/injection/mapping_handlers/axis_switch_handler.py +++ b/inputremapper/injection/mapping_handlers/axis_switch_handler.py @@ -24,6 +24,7 @@ from inputremapper.configs.input_config import InputCombination from inputremapper.configs.input_config import InputConfig from inputremapper.configs.mapping import Mapping +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, HandlerEnums, @@ -59,9 +60,10 @@ def __init__( combination: InputCombination, mapping: Mapping, context: ContextProtocol, + global_uinputs: GlobalUInputs, **_, ): - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) trigger_keys = tuple( event.input_match_hash for event in combination diff --git a/inputremapper/injection/mapping_handlers/combination_handler.py b/inputremapper/injection/mapping_handlers/combination_handler.py index 0ec784867..52429e251 100644 --- a/inputremapper/injection/mapping_handlers/combination_handler.py +++ b/inputremapper/injection/mapping_handlers/combination_handler.py @@ -26,6 +26,7 @@ from inputremapper.configs.input_config import InputCombination from inputremapper.configs.mapping import Mapping +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, InputEventHandler, @@ -52,10 +53,11 @@ def __init__( combination: InputCombination, mapping: Mapping, context: Context, + global_uinputs: GlobalUInputs, **_, ) -> None: logger.debug(str(mapping)) - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) self._pressed_keys = {} self._output_state = False self._context = context diff --git a/inputremapper/injection/mapping_handlers/hierarchy_handler.py b/inputremapper/injection/mapping_handlers/hierarchy_handler.py index 4f71581eb..2cc80f5ee 100644 --- a/inputremapper/injection/mapping_handlers/hierarchy_handler.py +++ b/inputremapper/injection/mapping_handlers/hierarchy_handler.py @@ -23,6 +23,7 @@ from evdev.ecodes import EV_ABS, EV_REL from inputremapper.configs.input_config import InputCombination, InputConfig +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, InputEventHandler, @@ -41,14 +42,17 @@ class HierarchyHandler(MappingHandler): _input_config: InputConfig def __init__( - self, handlers: List[MappingHandler], input_config: InputConfig + self, + handlers: List[MappingHandler], + input_config: InputConfig, + global_uinputs: GlobalUInputs, ) -> None: self.handlers = handlers self._input_config = input_config combination = InputCombination([input_config]) # use the mapping from the first child TODO: find a better solution mapping = handlers[0].mapping - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) def __str__(self): return f"HierarchyHandler for {self._input_config}" diff --git a/inputremapper/injection/mapping_handlers/key_handler.py b/inputremapper/injection/mapping_handlers/key_handler.py index 7ce023fb4..871128506 100644 --- a/inputremapper/injection/mapping_handlers/key_handler.py +++ b/inputremapper/injection/mapping_handlers/key_handler.py @@ -23,7 +23,7 @@ from inputremapper.configs.input_config import InputCombination from inputremapper.configs.mapping import Mapping from inputremapper.exceptions import MappingParsingError -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, HandlerEnums, @@ -43,9 +43,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ): - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) maps_to = mapping.get_output_type_code() if not maps_to: raise MappingParsingError( @@ -71,7 +72,7 @@ def notify(self, event: InputEvent, *_, **__) -> bool: event_tuple = (*self._maps_to, event.value) try: - global_uinputs.write(event_tuple, self.mapping.target_uinput) + self.global_uinputs.write(event_tuple, self.mapping.target_uinput) self._active = bool(event.value) return True except exceptions.Error: @@ -81,7 +82,7 @@ def reset(self) -> None: logger.debug("resetting key_handler") if self._active: event_tuple = (*self._maps_to, 0) - global_uinputs.write(event_tuple, self.mapping.target_uinput) + self.global_uinputs.write(event_tuple, self.mapping.target_uinput) self._active = False def needs_wrapping(self) -> bool: diff --git a/inputremapper/injection/mapping_handlers/macro_handler.py b/inputremapper/injection/mapping_handlers/macro_handler.py index ed6af7103..e61a6bb0f 100644 --- a/inputremapper/injection/mapping_handlers/macro_handler.py +++ b/inputremapper/injection/mapping_handlers/macro_handler.py @@ -22,7 +22,7 @@ from inputremapper.configs.input_config import InputCombination from inputremapper.configs.mapping import Mapping -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.macros.macro import Macro from inputremapper.injection.macros.parse import parse from inputremapper.injection.mapping_handlers.mapping_handler import ( @@ -45,10 +45,11 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, *, context: ContextProtocol, ): - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) self._active = False assert self.mapping.output_symbol is not None self._macro = parse(self.mapping.output_symbol, context, mapping) @@ -79,7 +80,9 @@ def notify(self, event: InputEvent, *_, **__) -> bool: def handler(type_, code, value) -> None: """Handler for macros.""" - global_uinputs.write((type_, code, value), self.mapping.target_uinput) + self.global_uinputs.write( + (type_, code, value), self.mapping.target_uinput + ) asyncio.ensure_future(self.run_macro(handler)) return True diff --git a/inputremapper/injection/mapping_handlers/mapping_handler.py b/inputremapper/injection/mapping_handlers/mapping_handler.py index 87608a043..0eb6f7ecb 100644 --- a/inputremapper/injection/mapping_handlers/mapping_handler.py +++ b/inputremapper/injection/mapping_handlers/mapping_handler.py @@ -68,6 +68,7 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.exceptions import MappingParsingError +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.input_event import InputEvent from inputremapper.logging.logger import logger @@ -154,6 +155,7 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: """Initialize the handler @@ -167,6 +169,7 @@ def __init__( self.mapping = mapping self.input_configs = list(combination) self._sub_handler = None + self.global_uinputs = global_uinputs def notify( self, diff --git a/inputremapper/injection/mapping_handlers/mapping_parser.py b/inputremapper/injection/mapping_handlers/mapping_parser.py index 65012e833..9f2343002 100644 --- a/inputremapper/injection/mapping_handlers/mapping_parser.py +++ b/inputremapper/injection/mapping_handlers/mapping_parser.py @@ -29,6 +29,7 @@ from inputremapper.configs.preset import Preset from inputremapper.configs.system_mapping import DISABLE_CODE, DISABLE_NAME from inputremapper.exceptions import MappingParsingError +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.macros.parse import is_this_a_macro from inputremapper.injection.mapping_handlers.abs_to_abs_handler import AbsToAbsHandler from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler @@ -76,250 +77,285 @@ } -def parse_mappings(preset: Preset, context: ContextProtocol) -> EventPipelines: - """Create a dict with a list of MappingHandler for each InputEvent.""" - handlers = [] - for mapping in preset: - # start with the last handler in the chain, each mapping only has one output, - # but may have multiple inputs, therefore the last handler is a good starting - # point to assemble the pipeline - handler_enum = _get_output_handler(mapping) - constructor = mapping_handler_classes[handler_enum] - if not constructor: - logger.warning( - "a mapping handler '%s' for %s is not implemented", - handler_enum, - mapping.format_name(), - ) - continue +class MappingParser: + def __init__( + self, + global_uinputs: GlobalUInputs, + ): + self.global_uinputs = global_uinputs + + def parse_mappings( + self, + preset: Preset, + context: ContextProtocol, + ) -> EventPipelines: + """Create a dict with a list of MappingHandler for each InputEvent.""" + handlers = [] + for mapping in preset: + # start with the last handler in the chain, each mapping only has one output, + # but may have multiple inputs, therefore the last handler is a good starting + # point to assemble the pipeline + handler_enum = self._get_output_handler(mapping) + constructor = mapping_handler_classes[handler_enum] + if not constructor: + logger.warning( + "a mapping handler '%s' for %s is not implemented", + handler_enum, + mapping.format_name(), + ) + continue - output_handler = constructor( - mapping.input_combination, - mapping, - context=context, - ) + output_handler = constructor( + mapping.input_combination, + mapping, + context=context, + global_uinputs=self.global_uinputs, + ) - # layer other handlers on top until the outer handler needs ranking or can - # directly handle a input event - handlers.extend(_create_event_pipeline(output_handler, context)) + # layer other handlers on top until the outer handler needs ranking or can + # directly handle a input event + handlers.extend(self._create_event_pipeline(output_handler, context)) + + # figure out which handlers need ranking and wrap them with hierarchy_handlers + need_ranking = defaultdict(set) + for handler in handlers.copy(): + if handler.needs_ranking(): + combination = handler.rank_by() + if not combination: + raise MappingParsingError( + f"{type(handler).__name__} claims to need ranking but does not " + f"return a combination to rank by", + mapping_handler=handler, + ) + + need_ranking[combination].add(handler) + handlers.remove(handler) + + # the HierarchyHandler's might not be the starting point of the event pipeline, + # layer other handlers on top again. + ranked_handlers = self._create_hierarchy_handlers(need_ranking) + for handler in ranked_handlers: + handlers.extend( + self._create_event_pipeline(handler, context, ignore_ranking=True) + ) - # figure out which handlers need ranking and wrap them with hierarchy_handlers - need_ranking = defaultdict(set) - for handler in handlers.copy(): - if handler.needs_ranking(): - combination = handler.rank_by() - if not combination: - raise MappingParsingError( - f"{type(handler).__name__} claims to need ranking but does not " - f"return a combination to rank by", - mapping_handler=handler, + # group all handlers by the input events they take care of. One handler might end + # up in multiple groups if it takes care of multiple InputEvents + event_pipelines: EventPipelines = defaultdict(set) + for handler in handlers: + assert handler.input_configs + for input_config in handler.input_configs: + logger.debug( + "event-pipeline with entry point: %s %s", + get_evdev_constant_name(*input_config.type_and_code), + input_config.input_match_hash, + ) + logger.debug_mapping_handler(handler) + event_pipelines[input_config].add(handler) + + return event_pipelines + + def _create_event_pipeline( + self, + handler: MappingHandler, + context: ContextProtocol, + ignore_ranking=False, + ) -> List[MappingHandler]: + """Recursively wrap a handler with other handlers until the + outer handler needs ranking or is finished wrapping. + """ + if not handler.needs_wrapping() or ( + handler.needs_ranking() and not ignore_ranking + ): + return [handler] + + handlers = [] + for combination, handler_enum in handler.wrap_with().items(): + constructor = mapping_handler_classes[handler_enum] + if not constructor: + raise NotImplementedError( + f"mapping handler {handler_enum} is not implemented" ) - need_ranking[combination].add(handler) - handlers.remove(handler) - - # the HierarchyHandler's might not be the starting point of the event pipeline, - # layer other handlers on top again. - ranked_handlers = _create_hierarchy_handlers(need_ranking) - for handler in ranked_handlers: - handlers.extend(_create_event_pipeline(handler, context, ignore_ranking=True)) - - # group all handlers by the input events they take care of. One handler might end - # up in multiple groups if it takes care of multiple InputEvents - event_pipelines: EventPipelines = defaultdict(set) - for handler in handlers: - assert handler.input_configs - for input_config in handler.input_configs: - logger.debug( - "event-pipeline with entry point: %s %s", - get_evdev_constant_name(*input_config.type_and_code), - input_config.input_match_hash, - ) - logger.debug_mapping_handler(handler) - event_pipelines[input_config].add(handler) - - return event_pipelines - - -def _create_event_pipeline( - handler: MappingHandler, context: ContextProtocol, ignore_ranking=False -) -> List[MappingHandler]: - """Recursively wrap a handler with other handlers until the - outer handler needs ranking or is finished wrapping. - """ - if not handler.needs_wrapping() or (handler.needs_ranking() and not ignore_ranking): - return [handler] - - handlers = [] - for combination, handler_enum in handler.wrap_with().items(): - constructor = mapping_handler_classes[handler_enum] - if not constructor: - raise NotImplementedError( - f"mapping handler {handler_enum} is not implemented" + super_handler = constructor( + combination, + handler.mapping, + context=context, + global_uinputs=self.global_uinputs, ) + super_handler.set_sub_handler(handler) + for event in combination: + # the handler now has a super_handler which takes care about the events. + # so we need to hide them on the handler + handler.occlude_input_event(event) - super_handler = constructor(combination, handler.mapping, context=context) - super_handler.set_sub_handler(handler) - for event in combination: - # the handler now has a super_handler which takes care about the events. - # so we need to hide them on the handler - handler.occlude_input_event(event) + handlers.extend(self._create_event_pipeline(super_handler, context)) - handlers.extend(_create_event_pipeline(super_handler, context)) + if handler.input_configs: + # the handler was only partially wrapped, + # we need to return it as a toplevel handler + handlers.append(handler) - if handler.input_configs: - # the handler was only partially wrapped, - # we need to return it as a toplevel handler - handlers.append(handler) + return handlers - return handlers + def _get_output_handler(self, mapping: Mapping) -> HandlerEnums: + """Determine the correct output handler. + this is used as a starting point for the mapping parser + """ + if mapping.output_code == DISABLE_CODE or mapping.output_symbol == DISABLE_NAME: + return HandlerEnums.disable -def _get_output_handler(mapping: Mapping) -> HandlerEnums: - """Determine the correct output handler. + if mapping.output_symbol: + if is_this_a_macro(mapping.output_symbol): + return HandlerEnums.macro - this is used as a starting point for the mapping parser - """ - if mapping.output_code == DISABLE_CODE or mapping.output_symbol == DISABLE_NAME: - return HandlerEnums.disable + return HandlerEnums.key - if mapping.output_symbol: - if is_this_a_macro(mapping.output_symbol): - return HandlerEnums.macro + if mapping.output_type == EV_KEY: + return HandlerEnums.key - return HandlerEnums.key + input_event = self._maps_axis(mapping.input_combination) + if not input_event: + raise MappingParsingError( + f"This {mapping = } does not map to an axis, key or macro", + mapping=Mapping, + ) - if mapping.output_type == EV_KEY: - return HandlerEnums.key + if mapping.output_type == EV_REL: + if input_event.type == EV_KEY: + return HandlerEnums.btn2rel + if input_event.type == EV_REL: + return HandlerEnums.rel2rel + if input_event.type == EV_ABS: + return HandlerEnums.abs2rel + + if mapping.output_type == EV_ABS: + if input_event.type == EV_KEY: + return HandlerEnums.btn2abs + if input_event.type == EV_REL: + return HandlerEnums.rel2abs + if input_event.type == EV_ABS: + return HandlerEnums.abs2abs - input_event = _maps_axis(mapping.input_combination) - if not input_event: raise MappingParsingError( - f"This {mapping = } does not map to an axis, key or macro", - mapping=Mapping, + f"the output of {mapping = } is unknown", mapping=Mapping ) - if mapping.output_type == EV_REL: - if input_event.type == EV_KEY: - return HandlerEnums.btn2rel - if input_event.type == EV_REL: - return HandlerEnums.rel2rel - if input_event.type == EV_ABS: - return HandlerEnums.abs2rel - - if mapping.output_type == EV_ABS: - if input_event.type == EV_KEY: - return HandlerEnums.btn2abs - if input_event.type == EV_REL: - return HandlerEnums.rel2abs - if input_event.type == EV_ABS: - return HandlerEnums.abs2abs - - raise MappingParsingError(f"the output of {mapping = } is unknown", mapping=Mapping) - - -def _maps_axis(combination: InputCombination) -> Optional[InputConfig]: - """Whether this InputCombination contains an InputEvent that is treated as - an axis and not a binary (key or button) event. - """ - for event in combination: - if event.defines_analog_input: - return event - return None - - -def _create_hierarchy_handlers( - handlers: Dict[InputCombination, Set[MappingHandler]] -) -> Set[MappingHandler]: - """Sort handlers by input events and create Hierarchy handlers.""" - sorted_handlers = set() - all_combinations = handlers.keys() - events = set() - - # gather all InputEvents from all handlers - for combination in all_combinations: + def _maps_axis(self, combination: InputCombination) -> Optional[InputConfig]: + """Whether this InputCombination contains an InputEvent that is treated as + an axis and not a binary (key or button) event. + """ for event in combination: - events.add(event) - - # create a ranking for each event - for event in events: - # find all combinations (from handlers) which contain the event - combinations_with_event = [ - combination for combination in all_combinations if event in combination - ] - - if len(combinations_with_event) == 1: - # there was only one handler containing that event return it as is - sorted_handlers.update(handlers[combinations_with_event[0]]) - continue - - # there are multiple handler with the same event. - # rank them and create the HierarchyHandler - sorted_combinations = _order_combinations(combinations_with_event, event) - sub_handlers: List[MappingHandler] = [] - for combination in sorted_combinations: - sub_handlers.append(*handlers[combination]) - - sorted_handlers.add(HierarchyHandler(sub_handlers, event)) - for handler in sub_handlers: - # the handler now has a HierarchyHandler which takes care about this event. - # so we hide need to hide it on the handler - handler.occlude_input_event(event) - - return sorted_handlers - - -def _order_combinations( - combinations: List[InputCombination], common_config: InputConfig -) -> List[InputCombination]: - """Reorder the keys according to some rules. - - such that a combination a+b+c is in front of a+b which is in front of b - for a+b+c vs. b+d+e: a+b+c would be in front of b+d+e, because the common key b - has the higher index in the a+b+c (1), than in the b+c+d (0) list - in this example b would be the common key - as for combinations like a+b+c and e+d+c with the common key c: ¯\\_(ツ)_/¯ - - Parameters - ---------- - combinations - the list which needs ordering - common_config - the InputConfig all InputCombination's in combinations have in common - """ - combinations.sort(key=len) - - for start, end in _ranges_with_constant_length(combinations.copy()): - sub_list = combinations[start:end] - sub_list.sort(key=lambda x: x.index(common_config)) - combinations[start:end] = sub_list - - combinations.reverse() - return combinations - - -def _ranges_with_constant_length(x: Sequence[Sized]) -> Iterable[Tuple[int, int]]: - """Get all ranges of x for which the elements have constant length - - Parameters - ---------- - x: Sequence[Sized] - l must be ordered by increasing length of elements - """ - start_idx = 0 - last_len = 0 - for idx, y in enumerate(x): - if len(y) > last_len and idx - start_idx > 1: - yield start_idx, idx - - if len(y) == last_len and idx + 1 == len(x): - yield start_idx, idx + 1 - - if len(y) > last_len: - start_idx = idx - - if len(y) < last_len: - raise MappingParsingError( - "ranges_with_constant_length was called with an unordered list" + if event.defines_analog_input: + return event + return None + + def _create_hierarchy_handlers( + self, + handlers: Dict[InputCombination, Set[MappingHandler]], + ) -> Set[MappingHandler]: + """Sort handlers by input events and create Hierarchy handlers.""" + sorted_handlers = set() + all_combinations = handlers.keys() + events = set() + + # gather all InputEvents from all handlers + for combination in all_combinations: + for event in combination: + events.add(event) + + # create a ranking for each event + for event in events: + # find all combinations (from handlers) which contain the event + combinations_with_event = [ + combination for combination in all_combinations if event in combination + ] + + if len(combinations_with_event) == 1: + # there was only one handler containing that event return it as is + sorted_handlers.update(handlers[combinations_with_event[0]]) + continue + + # there are multiple handler with the same event. + # rank them and create the HierarchyHandler + sorted_combinations = self._order_combinations( + combinations_with_event, + event, ) - last_len = len(y) + sub_handlers: List[MappingHandler] = [] + for combination in sorted_combinations: + sub_handlers.append(*handlers[combination]) + + sorted_handlers.add( + HierarchyHandler( + sub_handlers, + event, + self.global_uinputs, + ) + ) + for handler in sub_handlers: + # the handler now has a HierarchyHandler which takes care about this event. + # so we hide need to hide it on the handler + handler.occlude_input_event(event) + + return sorted_handlers + + def _order_combinations( + self, + combinations: List[InputCombination], + common_config: InputConfig, + ) -> List[InputCombination]: + """Reorder the keys according to some rules. + + such that a combination a+b+c is in front of a+b which is in front of b + for a+b+c vs. b+d+e: a+b+c would be in front of b+d+e, because the common key b + has the higher index in the a+b+c (1), than in the b+c+d (0) list + in this example b would be the common key + as for combinations like a+b+c and e+d+c with the common key c: ¯\\_(ツ)_/¯ + + Parameters + ---------- + combinations + the list which needs ordering + common_config + the InputConfig all InputCombination's in combinations have in common + """ + combinations.sort(key=len) + + for start, end in self._ranges_with_constant_length(combinations.copy()): + sub_list = combinations[start:end] + sub_list.sort(key=lambda x: x.index(common_config)) + combinations[start:end] = sub_list + + combinations.reverse() + return combinations + + def _ranges_with_constant_length( + self, + x: Sequence[Sized], + ) -> Iterable[Tuple[int, int]]: + """Get all ranges of x for which the elements have constant length + + Parameters + ---------- + x: Sequence[Sized] + l must be ordered by increasing length of elements + """ + start_idx = 0 + last_len = 0 + for idx, y in enumerate(x): + if len(y) > last_len and idx - start_idx > 1: + yield start_idx, idx + + if len(y) == last_len and idx + 1 == len(x): + yield start_idx, idx + 1 + + if len(y) > last_len: + start_idx = idx + + if len(y) < last_len: + raise MappingParsingError( + "ranges_with_constant_length was called with an unordered list" + ) + last_len = len(y) diff --git a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py index e8b5ec6a4..ec6de883e 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py @@ -39,7 +39,7 @@ REL_XY_SCALING, DEFAULT_REL_RATE, ) -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, @@ -74,9 +74,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) # find the input event we are supposed to map. If the input combination is # BTN_A + REL_X + BTN_B, then use the value of REL_X for the transformation @@ -227,7 +228,7 @@ def _scale_to_target(self, x: float) -> int: def _write(self, value: int) -> None: """Inject.""" try: - global_uinputs.write( + self.global_uinputs.write( (*self._output_axis, value), self.mapping.target_uinput ) except OverflowError: diff --git a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py index bbc39d6c8..387de3823 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py @@ -25,6 +25,7 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, InputEventHandler, @@ -49,9 +50,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) self._active = False self._input_config = combination[0] diff --git a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py index d41a2c465..227c89e06 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py @@ -37,7 +37,7 @@ WHEEL_SCALING, WHEEL_HI_RES_SCALING, ) -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, @@ -91,9 +91,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) assert self.mapping.output_code is not None @@ -256,7 +257,7 @@ def _write(self, code: int, value: int): if value == 0: return - global_uinputs.write( + self.global_uinputs.write( (EV_REL, code, value), self.mapping.target_uinput, ) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index 3548bdb74..89e790592 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -46,13 +46,14 @@ get_incomplete_parameter, get_incomplete_function_name, ) +from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from tests.integration.test_components import FlowBoxTestUtils from tests.lib.cleanup import cleanup from tests.lib.constants import EVENT_READ_TIMEOUT from tests.lib.fixtures import fixtures from tests.lib.fixtures import prepare_presets -from tests.lib.global_uinputs import reset_global_uinputs_for_service from tests.lib.logger import logger from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe from tests.lib.project_root import get_project_root @@ -140,7 +141,8 @@ def launch( def start_reader_service(): def process(): - reader_service = ReaderService(_Groups()) + global_uinputs = GlobalUInputs(FrontendUInput) + reader_service = ReaderService(_Groups(), global_uinputs) loop = asyncio.new_event_loop() loop.run_until_complete(reader_service.run()) @@ -162,15 +164,25 @@ def os_system_patch(cmd, original_os_system=os.system): def patch_launch(): """patch the launch function such that we don't connect to the dbus and don't use pkexec to start the reader-service""" + + def bootstrap_daemon(): + # The daemon gets fresh instances of everything, because as far as I remember + # it runs in a separate process. + global_config = GlobalConfig() + global_uinputs = GlobalUInputs(UInput) + mapping_parser = MappingParser(global_uinputs) + + return Daemon( + global_config, + global_uinputs, + mapping_parser, + ) + with patch.object( os, "system", os_system_patch, - ), patch.object( - Daemon, - "connect", - lambda: Daemon(GlobalConfig()), - ): + ), patch.object(Daemon, "connect", bootstrap_daemon): yield @@ -221,12 +233,25 @@ def os_system(cmd, original_os_system=os.system): # time for the application self.os_system_patch.start() + def bootstrap_daemon(self): + # The daemon gets fresh instances of everything, because as far as I remember + # it runs in a separate process. + global_config = GlobalConfig() + global_uinputs = GlobalUInputs(UInput) + mapping_parser = MappingParser(global_uinputs) + + return Daemon( + global_config, + global_uinputs, + mapping_parser, + ) + def patch_daemon(self): # don't try to connect, return an object instance of it instead self.daemon_connect_patch = patch.object( Daemon, "connect", - lambda: Daemon(GlobalConfig()), + lambda: self.bootstrap_daemon(), ) self.daemon_connect_patch.start() @@ -1791,11 +1816,6 @@ def test_cannot_record_keys(self): self.assertIn("Stop", text) def test_start_injecting(self): - # It's 2023 everyone! That means this test randomly stopped working because it - # used FrontendUInputs instead of regular UInputs. I guess a fucking ghost - # was fixing this for us during 2022, but it seems to have disappeared. - reset_global_uinputs_for_service() - self.controller.load_group("Foo Device 2") with spy(self.daemon, "set_config_dir") as spy1: @@ -1850,8 +1870,6 @@ def test_start_injecting(self): self.assertNotIn("input-remapper", device_group_entry.name) def test_stop_injecting(self): - reset_global_uinputs_for_service() - self.controller.load_group("Foo Device 2") self.start_injector_btn.clicked() gtk_iteration() diff --git a/tests/lib/cleanup.py b/tests/lib/cleanup.py index ff2cd29df..18ffc7515 100644 --- a/tests/lib/cleanup.py +++ b/tests/lib/cleanup.py @@ -84,8 +84,7 @@ def quick_cleanup(log=True): from inputremapper.injection.macros.macro import macro_variables from inputremapper.configs.system_mapping import system_mapping from inputremapper.gui.utils import debounce_manager - from inputremapper.injection.global_uinputs import global_uinputs - from tests.lib.global_uinputs import reset_global_uinputs_for_service + from inputremapper.injection.global_uinputs import GlobalUInputs if log: logger.info("Quick cleanup...") @@ -150,11 +149,6 @@ def quick_cleanup(log=True): assert not pipe.poll() assert macro_variables.is_alive(1) - for uinput in global_uinputs.devices.values(): - uinput.write_count = 0 - uinput.write_history = [] - - reset_global_uinputs_for_service() if log: logger.info("Quick cleanup done") diff --git a/tests/lib/global_uinputs.py b/tests/lib/global_uinputs.py deleted file mode 100644 index cf6608c3c..000000000 --- a/tests/lib/global_uinputs.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -# input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2024 sezanzeb -# -# This file is part of input-remapper. -# -# input-remapper is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# input-remapper is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with input-remapper. If not, see . - -import sys -from unittest.mock import patch - -from inputremapper.injection.global_uinputs import global_uinputs - - -def reset_global_uinputs_for_service(): - with patch.object(sys, "argv", ["input-remapper-service"]): - # patch argv for global_uinputs to think it is a service - global_uinputs.reset() - - -def reset_global_uinputs_for_gui(): - with patch.object(sys, "argv", ["input-remapper-gtk"]): - global_uinputs.reset() diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index 87b6dacd6..650606177 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -33,6 +33,8 @@ from inputremapper.configs.mapping import Mapping from inputremapper.configs.preset import Preset from inputremapper.injection.context import Context +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from tests.lib.test_setup import test_setup @@ -40,6 +42,9 @@ @test_setup class TestContext(unittest.TestCase): def test_callbacks(self): + global_uinputs = GlobalUInputs(UInput) + mapping_parser = MappingParser(global_uinputs) + preset = Preset() cfg = { "input_combination": InputCombination.from_tuples((EV_ABS, ABS_X)), @@ -81,7 +86,7 @@ def test_callbacks(self): ), ) - context = Context(preset, {}, {}) + context = Context(preset, {}, {}, mapping_parser) expected_num_callbacks = { # ABS_X -> "d" and ABS_X -> wheel have the same type and code diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index 8c43c7608..8e377aa4b 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -29,10 +29,13 @@ from unittest.mock import patch from inputremapper.configs.global_config import GlobalConfig +from inputremapper.configs.migrations import Migrations from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.daemon import Daemon from inputremapper.groups import groups +from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from tests.lib.test_setup import test_setup from tests.lib.tmp import tmp @@ -67,7 +70,12 @@ def import_control(): class TestControl(unittest.TestCase): def setUp(self): self.global_config = GlobalConfig() - self.input_remapper_control = InputRemapperControl(self.global_config) + self.global_uinputs = GlobalUInputs(FrontendUInput) + self.migrations = Migrations(self.global_uinputs) + self.mapping_parser = MappingParser(self.global_uinputs) + self.input_remapper_control = InputRemapperControl( + self.global_config, self.migrations + ) def test_autoload(self): device_keys = ["Foo Device 2", "Bar Device"] @@ -83,7 +91,7 @@ def test_autoload(self): Preset(paths[1]).save() Preset(paths[2]).save() - daemon = Daemon(self.global_config) + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.input_remapper_control.set_daemon(daemon) @@ -238,7 +246,7 @@ def test_autoload_other_path(self): Preset(paths[0]).save() Preset(paths[1]).save() - daemon = Daemon(self.global_config) + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.input_remapper_control.set_daemon(daemon) start_history = [] @@ -264,7 +272,7 @@ def test_start_stop(self): group = groups.find(key="Foo Device 2") preset = "preset9" - daemon = Daemon(self.global_config) + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.input_remapper_control.set_daemon(daemon) start_history = [] @@ -308,7 +316,7 @@ def test_config_not_found(self): path = "~/a/preset.json" config_dir = "/foo/bar" - daemon = Daemon(self.global_config) + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.input_remapper_control.set_daemon(daemon) start_history = [] @@ -337,7 +345,7 @@ def test_config_not_found(self): ) def test_autoload_config_dir(self): - daemon = Daemon(self.global_config) + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) path = os.path.join(tmp, "foo") os.makedirs(path) diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index 39015835d..66e387525 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -50,7 +50,7 @@ from inputremapper.gui.reader_client import ReaderClient from inputremapper.gui.utils import CTX_ERROR, CTX_APPLY, gtk_iteration from inputremapper.gui.gettext import _ -from inputremapper.injection.global_uinputs import GlobalUInputs +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput, FrontendUInput from inputremapper.configs.mapping import UIMapping, MappingData, Mapping from tests.lib.spy import spy from tests.lib.patches import FakeDaemonProxy @@ -68,7 +68,7 @@ class TestController(unittest.TestCase): def setUp(self) -> None: super().setUp() self.message_broker = MessageBroker() - uinputs = GlobalUInputs() + uinputs = GlobalUInputs(FrontendUInput) uinputs.prepare_all() self.data_manager = DataManager( self.message_broker, diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index 327a81f41..3eb77dace 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -37,8 +37,9 @@ from inputremapper.configs.system_mapping import system_mapping from inputremapper.daemon import Daemon from inputremapper.groups import groups -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput from inputremapper.injection.injector import InjectorState +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from tests.lib.cleanup import cleanup from tests.lib.fixtures import Fixture @@ -56,12 +57,14 @@ class TestDaemon(unittest.TestCase): def setUp(self): self.daemon = None self.global_config = GlobalConfig() + self.global_uinputs = GlobalUInputs(UInput) PathUtils.mkdir(PathUtils.get_config_path()) self.global_config._save_config() + self.mapping_parser = MappingParser(self.global_uinputs) # the daemon should be able to create them on demand: - global_uinputs.devices = {} - global_uinputs.is_service = True + self.global_uinputs.devices = {} + self.global_uinputs.is_service = True def tearDown(self): # avoid race conditions with other tests, daemon may run processes @@ -142,19 +145,23 @@ def test_daemon(self): [InputEvent.key(BTN_B, 1, fixtures.gamepad.get_device_hash())], ) - self.daemon = Daemon(self.global_config) + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) self.assertFalse(uinput_write_history_pipe[0].poll()) # has been cleanedUp in setUp - self.assertNotIn("keyboard", global_uinputs.devices) + self.assertNotIn("keyboard", self.global_uinputs.devices) logger.info(f"start injector for {group.key}") self.daemon.start_injecting(group.key, preset_name) # created on demand - self.assertIn("keyboard", global_uinputs.devices) - self.assertNotIn("gamepad", global_uinputs.devices) + self.assertIn("keyboard", self.global_uinputs.devices) + self.assertNotIn("gamepad", self.global_uinputs.devices) self.assertEqual(self.daemon.get_state(group.key), InjectorState.STARTING) self.assertEqual(self.daemon.get_state(group2.key), InjectorState.UNKNOWN) @@ -206,7 +213,11 @@ def test_config_dir(self): # freshly loads the config and therefore removes the previosly added key. # This is important so that if the service is started via sudo or pkexec # it knows where to look for configuration files. - self.daemon = Daemon(self.global_config) + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) self.assertEqual(self.daemon.config_dir, PathUtils.get_config_path()) self.assertIsNone(self.global_config.get("foo")) @@ -244,7 +255,11 @@ def test_refresh_on_start(self): preset.save() self.global_config.set_autoload_preset(group_key, preset_name) - self.daemon = Daemon(self.global_config) + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) # make sure the devices are populated groups.refresh() @@ -281,7 +296,11 @@ def test_refresh_for_unknown_key(self): # this test only makes sense if this device is unknown yet self.assertIsNone(groups.find(name=device_9876)) - self.daemon = Daemon(self.global_config) + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) # make sure the devices are populated groups.refresh() @@ -353,7 +372,11 @@ def test_xmodmap_file(self): # test setup complete - self.daemon = Daemon(self.global_config) + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) self.daemon.set_config_dir(config_dir) self.daemon.start_injecting(group.key, preset_name) @@ -371,7 +394,11 @@ def test_start_stop(self): group = groups.find(key=group_key) preset_name = "preset8" - daemon = Daemon(self.global_config) + daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) self.daemon = daemon pereset = Preset(group.get_preset_path(preset_name)) @@ -439,7 +466,7 @@ def test_autoload(self): group_key = "Qux/Device?" group = groups.find(key=group_key) - daemon = Daemon(self.global_config) + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.daemon = daemon preset = Preset(group.get_preset_path(preset_name)) @@ -496,7 +523,11 @@ def test_autoload(self): self.assertEqual(len_before, len_after) def test_autoload_2(self): - self.daemon = Daemon(self.global_config) + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) history = self.daemon.autoload_history._autoload_history # existing device @@ -537,7 +568,11 @@ def test_autoload_3(self): self.global_config.set_autoload_preset(group.key, preset_name) - self.daemon = Daemon(self.global_config) + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) groups.set_groups([]) # caused the bug self.assertIsNone(groups.find(key="Foo Device 2")) self.daemon.autoload() diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index a1960736a..5f07fba0a 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -43,7 +43,7 @@ CombinationUpdate, ) from inputremapper.gui.reader_client import ReaderClient -from inputremapper.injection.global_uinputs import GlobalUInputs +from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput from tests.lib.fixtures import prepare_presets from tests.lib.patches import FakeDaemonProxy from tests.lib.test_setup import test_setup @@ -62,7 +62,7 @@ class TestDataManager(unittest.TestCase): def setUp(self) -> None: self.message_broker = MessageBroker() self.reader = ReaderClient(self.message_broker, _Groups()) - self.uinputs = GlobalUInputs() + self.uinputs = GlobalUInputs(FrontendUInput) self.uinputs.prepare_all() self.global_config = GlobalConfig() self.data_manager = DataManager( diff --git a/tests/unit/test_event_pipeline/test_event_pipeline.py b/tests/unit/test_event_pipeline/test_event_pipeline.py index 97271993c..2b2b10ae6 100644 --- a/tests/unit/test_event_pipeline/test_event_pipeline.py +++ b/tests/unit/test_event_pipeline/test_event_pipeline.py @@ -58,7 +58,8 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from tests.lib.cleanup import cleanup from tests.lib.logger import logger @@ -71,8 +72,11 @@ class EventPipelineTestBase(unittest.IsolatedAsyncioTestCase): """Test the event pipeline form event_reader to UInput.""" def setUp(self): - global_uinputs.is_service = True - global_uinputs.prepare_all() + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() + self.mapping_parser = MappingParser(self.global_uinputs) + self.global_uinputs.is_service = True + self.global_uinputs.prepare_all() self.forward_uinput = evdev.UInput() self.stop_event = asyncio.Event() @@ -100,6 +104,7 @@ def create_event_reader( preset, source_devices={}, forward_devices={source.get_device_hash(): self.forward_uinput}, + mapping_parser=self.mapping_parser, ) reader = EventReader( context, @@ -213,7 +218,7 @@ async def test_any_event_as_button(self): # wait a bit for the rel_to_btn handler to send the key up await asyncio.sleep(0.1) - history = global_uinputs.get_uinput("keyboard").write_history + history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(history.count((EV_KEY, code_b, 1)), 1) self.assertEqual(history.count((EV_KEY, code_c, 1)), 1) @@ -267,7 +272,7 @@ async def test_reset_releases_keys(self): await asyncio.sleep(0.1) forwarded_history = self.forward_uinput.write_history - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(len(forwarded_history), 0) # a down, b down, c down, d down @@ -277,7 +282,7 @@ async def test_reset_releases_keys(self): await asyncio.sleep(0.1) forwarded_history = self.forward_uinput.write_history - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(len(forwarded_history), 0) # all a, b, c, d down+up @@ -317,7 +322,7 @@ async def test_forward_abs(self): ) history = self.forward_uinput.write_history - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(history.count((EV_ABS, ABS_X, 10)), 1) self.assertEqual(history.count((EV_ABS, ABS_Y, 20)), 1) @@ -356,7 +361,7 @@ async def test_forward_rel(self): await asyncio.sleep(0.1) history = self.forward_uinput.write_history - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(history.count((EV_REL, REL_X, 10)), 1) self.assertEqual(history.count((EV_REL, REL_Y, 20)), 1) @@ -439,7 +444,7 @@ async def test_combination(self): event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history @@ -461,7 +466,7 @@ async def test_combination(self): event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertNotIn((EV_KEY, a, 1), keyboard_history) self.assertNotIn((EV_KEY, a, 0), keyboard_history) @@ -494,7 +499,7 @@ async def test_ignore_hold(self): event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(len(keyboard_history), 2) self.assertEqual(len(forwarded_history), 0) @@ -576,7 +581,7 @@ async def test_ignore_disabled(self): ], event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertIn((EV_KEY, a, 1), keyboard_history) self.assertIn((EV_KEY, a, 0), keyboard_history) @@ -586,7 +591,7 @@ async def test_ignore_disabled(self): """A combination that ends in a disabled key""" # ev_5 should be forwarded and the combination triggered await self.send_events(combi_1, event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertIn((EV_KEY, b, 1), keyboard_history) self.assertEqual(len(keyboard_history), 3) @@ -596,7 +601,7 @@ async def test_ignore_disabled(self): # release what the combination maps to await self.send_events([ev_4, ev_6], event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertIn((EV_KEY, b, 0), keyboard_history) self.assertEqual(len(keyboard_history), 4) @@ -606,7 +611,7 @@ async def test_ignore_disabled(self): """A combination that starts with a disabled key""" # only the combination should get triggered await self.send_events(combi_2, event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertIn((EV_KEY, c, 1), keyboard_history) self.assertEqual(len(keyboard_history), 5) @@ -616,7 +621,7 @@ async def test_ignore_disabled(self): # release what the combination maps to await self.send_events([ev_4, ev_6], event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history for event in keyboard_history: print(event.event_tuple) @@ -655,7 +660,7 @@ async def test_combination_keycode_macro_mix(self): # macro starts await self.send_events([InputEvent.from_tuple(down_1)], event_reader) await asyncio.sleep(0.05) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(len(forwarded_history), 0) self.assertGreater(len(keyboard_history), 1) @@ -666,29 +671,29 @@ async def test_combination_keycode_macro_mix(self): # combination triggered await self.send_events([InputEvent.from_tuple(down_2)], event_reader) await asyncio.sleep(0) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertIn((EV_KEY, b, 1), keyboard_history) - len_a = len(global_uinputs.get_uinput("keyboard").write_history) + len_a = len(self.global_uinputs.get_uinput("keyboard").write_history) await asyncio.sleep(0.05) - len_b = len(global_uinputs.get_uinput("keyboard").write_history) + len_b = len(self.global_uinputs.get_uinput("keyboard").write_history) # still running self.assertGreater(len_b, len_a) # release await self.send_events([InputEvent.from_tuple(up_1)], event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history[-1], (EV_KEY, b, 0)) await asyncio.sleep(0.05) - len_c = len(global_uinputs.get_uinput("keyboard").write_history) + len_c = len(self.global_uinputs.get_uinput("keyboard").write_history) await asyncio.sleep(0.05) - len_d = len(global_uinputs.get_uinput("keyboard").write_history) + len_d = len(self.global_uinputs.get_uinput("keyboard").write_history) # not running anymore self.assertEqual(len_c, len_d) await self.send_events([InputEvent.from_tuple(up_2)], event_reader) await asyncio.sleep(0.05) - len_e = len(global_uinputs.get_uinput("keyboard").write_history) + len_e = len(self.global_uinputs.get_uinput("keyboard").write_history) self.assertEqual(len_e, len_d) async def test_wheel_combination_release_failure(self): @@ -735,18 +740,18 @@ async def test_wheel_combination_release_failure(self): await self.send_events([scroll], event_reader) # "maps to 30" - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history[0], (EV_KEY, a, 1)) await self.send_events([scroll] * 5, event_reader) # nothing new since all of them were duplicate key downs - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(len(keyboard_history), 1) await self.send_events([btn_up], event_reader) # releasing the combination - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history[1], (EV_KEY, a, 0)) # more scroll events @@ -800,7 +805,7 @@ async def test_can_not_map(self): event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(len(forwarded_history), 4) @@ -821,7 +826,7 @@ async def test_axis_switch(self): rel_rate = 60 # rate [Hz] at which events are produced gain = 0.5 # halve the speed of the rel axis preset = Preset() - mouse = global_uinputs.get_uinput("mouse") + mouse = self.global_uinputs.get_uinput("mouse") forward_history = self.forward_uinput.write_history mouse_history = mouse.write_history @@ -976,7 +981,7 @@ async def test_abs_to_abs(self): await asyncio.sleep(0.2) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1026,7 +1031,7 @@ async def test_abs_to_abs_with_input_switch(self): await asyncio.sleep(0.2) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1083,7 +1088,7 @@ def next_usec_time(): await asyncio.sleep(0.1) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1102,7 +1107,7 @@ def next_usec_time(): event_reader, ) await asyncio.sleep(0.7) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1163,7 +1168,7 @@ async def test_rel_to_abs_with_input_switch(self): await asyncio.sleep(0.2) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1229,7 +1234,7 @@ async def test_abs_to_rel(self): event_reader, ) - mouse_history = global_uinputs.get_uinput("mouse").write_history + mouse_history = self.global_uinputs.get_uinput("mouse").write_history if mouse_history[0].type == EV_ABS: raise AssertionError( @@ -1303,7 +1308,7 @@ async def test_abs_to_wheel_hi_res_quirk(self): ], event_reader, ) - m_history = global_uinputs.get_uinput("mouse").write_history + m_history = self.global_uinputs.get_uinput("mouse").write_history rel_wheel = sum([event.value for event in m_history if event.code == REL_WHEEL]) rel_wheel_hi_res = sum( @@ -1370,7 +1375,7 @@ async def test_rel_to_btn(self): # wait more than the release_timeout to make sure all handlers finish await asyncio.sleep(release_timeout * 1.2) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(keyboard_history.count((EV_KEY, code_b, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, code_c, 1)), 1) @@ -1422,7 +1427,7 @@ async def test_rel_trigger_threshold(self): event_reader, ) await asyncio.sleep(release_timeout * 1.5) # release a - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history, [(EV_KEY, a, 1), (EV_KEY, a, 0)]) self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1) @@ -1437,14 +1442,14 @@ async def test_rel_trigger_threshold(self): ], event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 2) self.assertEqual(keyboard_history.count((EV_KEY, b, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, b, 0)), 1) self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 1) await asyncio.sleep(release_timeout * 1.5) # release a - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 2) self.assertEqual( @@ -1494,7 +1499,7 @@ async def test_abs_trigger_threshold(self): ], event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1) self.assertNotIn((EV_KEY, a, 0), keyboard_history) @@ -1508,7 +1513,7 @@ async def test_abs_trigger_threshold(self): ], event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, b, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, b, 0)), 1) @@ -1516,7 +1521,7 @@ async def test_abs_trigger_threshold(self): # 0% release a await event_reader.handle(InputEvent.abs(ABS_X, 0)) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 1) self.assertEqual(len(forwarded_history), 0) @@ -1545,7 +1550,7 @@ async def _test(self, input_code, input_value, output_code, output_value, gain=1 event_reader, ) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertEqual(len(history), 1) self.assertEqual( @@ -1602,7 +1607,7 @@ async def test_x_to_hwheel(self): event_reader, ) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history # injects both REL_WHEEL and REL_WHEEL_HI_RES events self.assertEqual(len(history), 2) self.assertEqual( @@ -1629,7 +1634,7 @@ async def test_x_to_hwheel(self): async def test_remainder(self): preset = Preset() - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history # REL_WHEEL_HI_RES to REL_Y input_config = InputConfig(type=EV_REL, code=REL_WHEEL_HI_RES) diff --git a/tests/unit/test_event_pipeline/test_mapping_handlers.py b/tests/unit/test_event_pipeline/test_mapping_handlers.py index 1b3262740..361d5d335 100644 --- a/tests/unit/test_event_pipeline/test_mapping_handlers.py +++ b/tests/unit/test_event_pipeline/test_mapping_handlers.py @@ -48,7 +48,7 @@ from inputremapper.configs.mapping import Mapping, DEFAULT_REL_RATE from inputremapper.configs.input_config import InputCombination, InputConfig -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput from inputremapper.injection.mapping_handlers.abs_to_abs_handler import AbsToAbsHandler from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler from inputremapper.injection.mapping_handlers.abs_to_rel_handler import AbsToRelHandler @@ -99,6 +99,8 @@ def setUp(self): InputConfig(type=1, code=3), ) ) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = AxisSwitchHandler( input_combination, Mapping( @@ -108,6 +110,7 @@ def setUp(self): output_code=1, ), MagicMock(), + self.global_uinputs, ) @@ -117,6 +120,8 @@ def setUp(self): input_combination = InputCombination( [InputConfig(type=3, code=5, analog_threshold=10)] ) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = AbsToBtnHandler( input_combination, Mapping( @@ -124,6 +129,7 @@ def setUp(self): target_uinput="mouse", output_symbol="BTN_LEFT", ), + global_uinputs=self.global_uinputs, ) @@ -131,6 +137,8 @@ def setUp(self): class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = AbsToAbsHandler( input_combination, Mapping( @@ -139,6 +147,7 @@ def setUp(self): output_type=EV_ABS, output_code=ABS_X, ), + global_uinputs=self.global_uinputs, ) async def test_reset(self): @@ -147,7 +156,7 @@ async def test_reset(self): source=InputDevice("/dev/input/event15"), ) self.handler.reset() - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [InputEvent.from_tuple((3, 0, MAX_ABS)), InputEvent.from_tuple((3, 0, 0))], @@ -158,6 +167,8 @@ async def test_reset(self): class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)]) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = RelToAbsHandler( input_combination, Mapping( @@ -166,6 +177,7 @@ def setUp(self): output_type=EV_ABS, output_code=ABS_X, ), + self.global_uinputs, ) async def test_reset(self): @@ -174,7 +186,7 @@ async def test_reset(self): source=InputDevice("/dev/input/event15"), ) self.handler.reset() - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual(len(history), 2) # something large, doesn't matter @@ -222,6 +234,8 @@ async def test_rate_stays(self): class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = AbsToRelHandler( input_combination, Mapping( @@ -230,6 +244,7 @@ def setUp(self): output_type=EV_REL, output_code=REL_X, ), + self.global_uinputs, ) async def test_reset(self): @@ -241,10 +256,10 @@ async def test_reset(self): self.handler.reset() await asyncio.sleep(0.05) - count = global_uinputs.get_uinput("mouse").write_count + count = self.global_uinputs.get_uinput("mouse").write_count self.assertGreater(count, 6) # count should be 60*0.2 = 12 await asyncio.sleep(0.2) - self.assertEqual(count, global_uinputs.get_uinput("mouse").write_count) + self.assertEqual(count, self.global_uinputs.get_uinput("mouse").write_count) @test_setup @@ -286,6 +301,8 @@ def setUp(self): self.context_mock = MagicMock() + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = CombinationHandler( input_combination, Mapping( @@ -294,6 +311,7 @@ def setUp(self): output_symbol="BTN_LEFT", ), self.context_mock, + global_uinputs=self.global_uinputs, ) def test_forward_correctly(self): @@ -394,9 +412,12 @@ def setUp(self): self.mock1 = MagicMock() self.mock2 = MagicMock() self.mock3 = MagicMock() + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = HierarchyHandler( [self.mock1, self.mock2, self.mock3], InputConfig(type=EV_KEY, code=KEY_A), + self.global_uinputs, ) def test_reset(self): @@ -415,6 +436,8 @@ def setUp(self): InputConfig(type=1, code=3), ) ) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = KeyHandler( input_combination, Mapping( @@ -422,6 +445,7 @@ def setUp(self): target_uinput="mouse", output_symbol="BTN_LEFT", ), + self.global_uinputs, ) def test_reset(self): @@ -429,12 +453,12 @@ def test_reset(self): InputEvent(0, 0, EV_REL, REL_X, 1, actions=(EventActions.as_key,)), source=InputDevice("/dev/input/event11"), ) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertEqual(history[0], InputEvent.key(BTN_LEFT, 1)) self.assertEqual(len(history), 1) self.handler.reset() - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertEqual(history[1], InputEvent.key(BTN_LEFT, 0)) self.assertEqual(len(history), 2) @@ -449,6 +473,8 @@ def setUp(self): ) ) self.context_mock = MagicMock() + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = MacroHandler( input_combination, Mapping( @@ -457,6 +483,7 @@ def setUp(self): output_symbol="hold_keys(BTN_LEFT, BTN_RIGHT)", ), context=self.context_mock, + global_uinputs=self.global_uinputs, ) async def test_reset(self): @@ -466,14 +493,14 @@ async def test_reset(self): ) await asyncio.sleep(0.1) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertIn(InputEvent.key(BTN_LEFT, 1), history) self.assertIn(InputEvent.key(BTN_RIGHT, 1), history) self.assertEqual(len(history), 2) self.handler.reset() await asyncio.sleep(0.1) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertIn(InputEvent.key(BTN_LEFT, 0), history[-2:]) self.assertIn(InputEvent.key(BTN_RIGHT, 0), history[-2:]) self.assertEqual(len(history), 4) @@ -485,6 +512,8 @@ def setUp(self): input_combination = InputCombination( [InputConfig(type=2, code=0, analog_threshold=10)] ) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = RelToBtnHandler( input_combination, Mapping( @@ -492,6 +521,7 @@ def setUp(self): target_uinput="mouse", output_symbol="BTN_LEFT", ), + self.global_uinputs, ) @@ -501,6 +531,8 @@ class TestRelToRelHanlder(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)]) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = RelToRelHandler( input_combination, Mapping( @@ -510,6 +542,7 @@ def setUp(self): output_value=20, target_uinput="mouse", ), + self.global_uinputs, ) def test_should_map(self): diff --git a/tests/unit/test_event_reader.py b/tests/unit/test_event_reader.py index c3e39a3ff..bef1bb58f 100644 --- a/tests/unit/test_event_reader.py +++ b/tests/unit/test_event_reader.py @@ -42,7 +42,8 @@ from inputremapper.configs.system_mapping import system_mapping from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from inputremapper.utils import get_device_hash from tests.lib.fixtures import fixtures @@ -53,15 +54,17 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase): def setUp(self): self.gamepad_source = evdev.InputDevice(fixtures.gamepad.path) + self.global_uinputs = GlobalUInputs(UInput) + self.mapping_parser = MappingParser(self.global_uinputs) self.stop_event = asyncio.Event() self.preset = Preset() - global_uinputs.is_service = True - global_uinputs.prepare_all() + self.global_uinputs.is_service = True + self.global_uinputs.prepare_all() async def setup(self, source, mapping): """Set a EventReader up for the test and run it in the background.""" - context = Context(mapping, {}, {}) + context = Context(mapping, {}, {}, self.mapping_parser) context.uinput = evdev.UInput() event_reader = EventReader(context, source, self.stop_event) asyncio.ensure_future(event_reader.run()) @@ -174,7 +177,7 @@ async def test_if_single_joystick_then(self): await asyncio.sleep(0.1) self.stop_event.set() # stop the reader - history = global_uinputs.get_uinput("keyboard").write_history + history = self.global_uinputs.get_uinput("keyboard").write_history self.assertIn((EV_KEY, code_a, 1), history) self.assertIn((EV_KEY, code_a, 0), history) self.assertNotIn((EV_KEY, code_shift, 1), history) @@ -233,7 +236,7 @@ async def test_if_single_joystick_under_threshold(self): ) await asyncio.sleep(0.1) self.assertEqual(len(context.listeners), 0) - history = global_uinputs.get_uinput("keyboard").write_history + history = self.global_uinputs.get_uinput("keyboard").write_history # the key that triggered if_single should be injected after # if_single had a chance to inject keys (if the macro is fast enough), diff --git a/tests/unit/test_global_uinputs.py b/tests/unit/test_global_uinputs.py index 458b8c1a7..6e5f94655 100644 --- a/tests/unit/test_global_uinputs.py +++ b/tests/unit/test_global_uinputs.py @@ -30,9 +30,9 @@ from inputremapper.exceptions import EventNotHandled, UinputNotAvailable from inputremapper.injection.global_uinputs import ( - global_uinputs, FrontendUInput, GlobalUInputs, + UInput, ) from inputremapper.input_event import InputEvent from tests.lib.cleanup import cleanup @@ -58,11 +58,12 @@ def test_init(self): @test_setup -class TestGlobalUinputs(unittest.TestCase): +class TestGlobalUInputs(unittest.TestCase): def setUp(self) -> None: cleanup() def test_iter(self): + global_uinputs = GlobalUInputs(FrontendUInput) for uinput in global_uinputs: self.assertIsInstance(uinput, evdev.UInput) @@ -71,6 +72,9 @@ def test_write(self): implicitly tests get_uinput and UInput.can_emit """ + global_uinputs = GlobalUInputs(UInput) + global_uinputs.prepare_all() + ev_1 = InputEvent.key(KEY_A, 1) ev_2 = InputEvent.abs(ABS_X, 10) @@ -86,9 +90,13 @@ def test_write(self): global_uinputs.write(ev_1.event_tuple, "foo") def test_creates_frontend_uinputs(self): - frontend_uinputs = GlobalUInputs() - with patch.object(sys, "argv", ["foo"]): - frontend_uinputs.prepare_all() - + frontend_uinputs = GlobalUInputs(FrontendUInput) + frontend_uinputs.prepare_all() uinput = frontend_uinputs.get_uinput("keyboard") self.assertIsInstance(uinput, FrontendUInput) + + def test_creates_backend_service_uinputs(self): + frontend_uinputs = GlobalUInputs(UInput) + frontend_uinputs.prepare_all() + uinput = frontend_uinputs.get_uinput("keyboard") + self.assertIsInstance(uinput, UInput) diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index 9636b375c..736a9c159 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -17,6 +17,8 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser try: from pydantic.v1 import ValidationError @@ -87,6 +89,9 @@ def setUpClass(cls): def setUp(self): self.failed = 0 self.make_it_fail = 2 + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() + self.mapping_parser = MappingParser(self.global_uinputs) def grab_fail_twice(_): if self.failed < self.make_it_fail: @@ -107,7 +112,7 @@ def tearDown(self): evdev.InputDevice.grab = self.grab def initialize_injector(self, group, preset: Preset): - self.injector = Injector(group, preset) + self.injector = Injector(group, preset, self.mapping_parser) self.injector._devices = self.injector.group.get_devices() self.injector._update_preset() @@ -123,10 +128,14 @@ def test_grab(self): ) ) - self.injector = Injector(groups.find(key="Foo Device 2"), preset) + self.injector = Injector( + groups.find(key="Foo Device 2"), + preset, + self.mapping_parser, + ) # this test needs to pass around all other constraints of # _grab_device - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) device = self.injector._grab_device(evdev.InputDevice(path)) gamepad = classify(device) == DeviceType.GAMEPAD self.assertFalse(gamepad) @@ -145,9 +154,13 @@ def test_fail_grab(self): ) ) - self.injector = Injector(groups.find(key="Foo Device 2"), preset) + self.injector = Injector( + groups.find(key="Foo Device 2"), + preset, + self.mapping_parser, + ) path = "/dev/input/event10" - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) device = self.injector._grab_device(evdev.InputDevice(path)) self.assertIsNone(device) self.assertGreaterEqual(self.failed, 1) @@ -182,7 +195,7 @@ def test_grab_device_1(self): ), ) self.initialize_injector(groups.find(name="gamepad"), preset) - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) self.injector.group.paths = [ "/dev/input/event10", "/dev/input/event30", @@ -209,7 +222,7 @@ def test_forward_gamepad_events(self): ) self.initialize_injector(groups.find(name="gamepad"), preset) - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) path = "/dev/input/event30" devices = self.injector._grab_devices() @@ -229,7 +242,7 @@ def test_skip_unused_device(self): ) ) self.initialize_injector(groups.find(key="Foo Device 2"), preset) - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) # grabs only one device even though the group has 4 devices devices = self.injector._grab_devices() @@ -248,7 +261,7 @@ def test_skip_unknown_device(self): # skips a device because its capabilities are not used in the preset self.initialize_injector(groups.find(key="Foo Device 2"), preset) - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) devices = self.injector._grab_devices() # skips the device alltogether, so no grab attempts fail @@ -256,7 +269,11 @@ def test_skip_unknown_device(self): self.assertEqual(devices, {}) def test_get_udev_name(self): - self.injector = Injector(groups.find(key="Foo Device 2"), Preset()) + self.injector = Injector( + groups.find(key="Foo Device 2"), + Preset(), + self.mapping_parser, + ) suffix = "mapped" prefix = "input-remapper" expected = f'{prefix} {"a" * (80 - len(suffix) - len(prefix) - 2)} {suffix}' @@ -301,7 +318,11 @@ def test_capabilities_and_uinput_presence(self, ungrab_patch): ) preset.add(m1) preset.add(m2) - self.injector = Injector(groups.find(key="Foo Device 2"), preset) + self.injector = Injector( + groups.find(key="Foo Device 2"), + preset, + self.mapping_parser, + ) self.injector.stop_injecting() self.injector.run() @@ -417,7 +438,11 @@ def test_injector(self): ) ) - self.injector = Injector(groups.find(key="Foo Device 2"), preset) + self.injector = Injector( + groups.find(key="Foo Device 2"), + preset, + self.mapping_parser, + ) self.assertEqual(self.injector.get_state(), InjectorState.UNKNOWN) self.injector.start() self.assertEqual(self.injector.get_state(), InjectorState.STARTING) @@ -544,6 +569,10 @@ def test_is_in_capabilities(self): @test_setup class TestModifyCapabilities(unittest.TestCase): def setUp(self): + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() + self.mapping_parser = MappingParser(self.global_uinputs) + class FakeDevice: def __init__(self): self._capabilities = { @@ -641,7 +670,7 @@ def check_keys(self, capabilities): def test_copy_capabilities(self): # I don't know what ABS_VOLUME is, for now I would like to just always # remove it until somebody complains, since its presence broke stuff - self.injector = Injector(mock.Mock(), self.preset) + self.injector = Injector(mock.Mock(), self.preset, self.mapping_parser) self.fake_device._capabilities = { EV_ABS: [ABS_VOLUME, (ABS_X, evdev.AbsInfo(0, 0, 500, 0, 0, 0))], EV_KEY: [1, 2, 3], diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index d05b2b78d..3dcfb55d2 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -46,6 +46,7 @@ SymbolNotAvailableInTargetError, ) from inputremapper.injection.context import Context +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput from inputremapper.injection.macros.macro import ( Macro, _type_check, @@ -67,6 +68,7 @@ get_macro_argument_names, get_num_parameters, ) +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from tests.lib.logger import logger from tests.lib.test_setup import test_setup @@ -75,6 +77,8 @@ class MacroTestBase(unittest.IsolatedAsyncioTestCase): def setUp(self): self.result = [] + self.global_uinputs = GlobalUInputs(UInput) + self.mapping_parser = MappingParser(self.global_uinputs) try: self.loop = asyncio.get_event_loop() @@ -84,7 +88,12 @@ def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) - self.context = Context(Preset(), source_devices={}, forward_devices={}) + self.context = Context( + Preset(), + source_devices={}, + forward_devices={}, + mapping_parser=self.mapping_parser, + ) def tearDown(self): self.result = [] diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index 27836b19e..92fa8d208 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -38,6 +38,7 @@ from inputremapper.configs.migrations import Migrations from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput from inputremapper.logging.logger import VERSION from inputremapper.user import UserUtils from tests.lib.test_setup import test_setup @@ -60,6 +61,10 @@ def setUp(self): UserUtils.home, ".config", "input-remapper", "beta_1.6.0-beta" ) + global_uinputs = GlobalUInputs(UInput) + global_uinputs.prepare_all() + self.migrations = Migrations(global_uinputs) + def test_migrate_suffix(self): old = os.path.join(PathUtils.config_path(), "config") new = os.path.join(PathUtils.config_path(), "config.json") @@ -73,7 +78,7 @@ def test_migrate_suffix(self): with open(old, "w") as f: f.write("{}") - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(new)) self.assertFalse(os.path.exists(old)) @@ -94,7 +99,7 @@ def test_rename_config(self): with open(old_config_json, "w") as f: f.write('{"foo":"bar"}') - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(new)) self.assertFalse(os.path.exists(old)) @@ -116,7 +121,7 @@ def test_wont_migrate_suffix(self): with open(old, "w") as f: f.write("{}") - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(new)) self.assertTrue(os.path.exists(old)) @@ -135,7 +140,7 @@ def test_migrate_preset(self): with open(p2, "w") as f: f.write("{}") - Migrations.migrate() + self.migrations.migrate() self.assertFalse( os.path.exists(os.path.join(PathUtils.config_path(), "foo1", "bar1.json")) @@ -173,7 +178,7 @@ def test_wont_migrate_preset(self): # already migrated PathUtils.mkdir(os.path.join(PathUtils.config_path(), "presets")) - Migrations.migrate() + self.migrations.migrate() self.assertTrue( os.path.exists(os.path.join(PathUtils.config_path(), "foo1", "bar1.json")) @@ -225,7 +230,7 @@ def test_migrate_mappings(self): }, file, ) - Migrations.migrate() + self.migrations.migrate() # use UIMapping to also load invalid mappings preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() @@ -332,7 +337,7 @@ def test_migrate_otherwise(self): file, ) - Migrations.migrate() + self.migrations.migrate() preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() @@ -384,9 +389,9 @@ def test_add_version(self): with open(path, "w") as file: file.write("{}") - Migrations.migrate() + self.migrations.migrate() self.assertEqual( - pkg_resources.parse_version(VERSION), Migrations.config_version() + pkg_resources.parse_version(VERSION), self.migrations.config_version() ) def test_update_version(self): @@ -395,9 +400,9 @@ def test_update_version(self): with open(path, "w") as file: json.dump({"version": "0.1.0"}, file) - Migrations.migrate() + self.migrations.migrate() self.assertEqual( - pkg_resources.parse_version(VERSION), Migrations.config_version() + pkg_resources.parse_version(VERSION), self.migrations.config_version() ) def test_config_version(self): @@ -405,14 +410,14 @@ def test_config_version(self): with open(path, "w") as file: file.write("{}") - self.assertEqual("0.0.0", Migrations.config_version().public) + self.assertEqual("0.0.0", self.migrations.config_version().public) try: os.remove(path) except FileNotFoundError: pass - self.assertEqual("0.0.0", Migrations.config_version().public) + self.assertEqual("0.0.0", self.migrations.config_version().public) def test_migrate_left_and_right_purpose(self): path = os.path.join( @@ -434,7 +439,7 @@ def test_migrate_left_and_right_purpose(self): }, file, ) - Migrations.migrate() + self.migrations.migrate() preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() @@ -520,7 +525,7 @@ def test_migrate_left_and_right_purpose2(self): }, file, ) - Migrations.migrate() + self.migrations.migrate() preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() @@ -636,7 +641,7 @@ def test_prioritize_v1_over_beta_configs(self): self.assertFalse(os.path.exists(PathUtils.get_preset_path(device_name, "foo"))) self.assertFalse(os.path.exists(PathUtils.get_config_path("config.json"))) - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(PathUtils.get_preset_path(device_name, "foo"))) self.assertTrue(os.path.exists(PathUtils.get_config_path("config.json"))) @@ -682,7 +687,7 @@ def test_copy_over_beta_configs(self): self.assertFalse(os.path.exists(PathUtils.get_preset_path(device_name, "bar"))) self.assertFalse(os.path.exists(PathUtils.get_config_path("config.json"))) - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(PathUtils.get_preset_path(device_name, "bar"))) self.assertTrue(os.path.exists(PathUtils.get_config_path("config.json"))) diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index c0e0b5e46..6cb72d362 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -52,6 +52,7 @@ from inputremapper.gui.messages.message_types import MessageType from inputremapper.gui.reader_client import ReaderClient from inputremapper.gui.reader_service import ReaderService, ContextDummy +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput, FrontendUInput from inputremapper.input_event import InputEvent from tests.lib.constants import ( EVENT_READ_TIMEOUT, @@ -109,7 +110,8 @@ async def create_reader_service(self, groups: Optional[_Groups] = None): if not groups: groups = self.groups - self.reader_service = ReaderService(groups) + global_uinputs = GlobalUInputs(UInput) + self.reader_service = ReaderService(groups, global_uinputs) asyncio.ensure_future(self.reader_service.run()) async def test_should_forward_to_dummy(self): @@ -164,6 +166,7 @@ def setUp(self): self.reader_service_process = None self.groups = _Groups() self.message_broker = MessageBroker() + self.global_uinputs = GlobalUInputs(UInput) self.reader_client = ReaderClient(self.message_broker, self.groups) def tearDown(self): @@ -182,10 +185,13 @@ def create_reader_service(self, groups: Optional[_Groups] = None): groups = self.groups def start_reader_service(): - reader_service = ReaderService(groups) - # this is a new process, so create a new event loop, or something + # this is a new process, so create a new event loop, and all dependencies + # from scratch, or something loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) + global_uinputs = GlobalUInputs(FrontendUInput) + global_uinputs.reset() + reader_service = ReaderService(groups, global_uinputs) loop.run_until_complete(reader_service.run()) self.reader_service_process = multiprocessing.Process( diff --git a/tests/unit/test_test.py b/tests/unit/test_test.py index 913eb1912..b57f48d69 100644 --- a/tests/unit/test_test.py +++ b/tests/unit/test_test.py @@ -31,6 +31,7 @@ from inputremapper.gui.messages.message_broker import MessageBroker from inputremapper.gui.reader_client import ReaderClient from inputremapper.gui.reader_service import ReaderService +from inputremapper.injection.global_uinputs import UInput, GlobalUInputs from inputremapper.input_event import InputEvent from inputremapper.utils import get_device_hash from tests.lib.cleanup import cleanup @@ -91,9 +92,10 @@ def create_reader_service(): # this will cause pending events to be copied over to the reader-service # process def start_reader_service(): - # there is no point in using the global groups object - # because the reader-service runs in a different process - reader_service = ReaderService(_Groups()) + # Create dependencies from scratch, because the reader-service runs + # in a different process + global_uinputs = GlobalUInputs(UInput) + reader_service = ReaderService(_Groups(), global_uinputs) loop = asyncio.new_event_loop() loop.run_until_complete(reader_service.run()) From 332837bb9f1de2799f7c7084063952184e59f875 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:49:08 +0200 Subject: [PATCH 28/32] rename SystemMapping to KeyboardLayout --- bin/input-remapper-control | 4 +- bin/input-remapper-gtk | 4 +- inputremapper/configs/input_config.py | 4 +- .../{system_mapping.py => keyboard_layout.py} | 8 +- inputremapper/configs/mapping.py | 10 +- inputremapper/configs/migrations.py | 4 +- inputremapper/configs/validation_errors.py | 8 +- inputremapper/daemon.py | 10 +- inputremapper/gui/autocompletion.py | 4 +- inputremapper/gui/components/editor.py | 6 +- inputremapper/gui/data_manager.py | 8 +- inputremapper/gui/utils.py | 5 +- inputremapper/injection/macros/macro.py | 4 +- .../mapping_handlers/mapping_parser.py | 2 +- tests/integration/test_components.py | 2 +- tests/integration/test_gui.py | 8 +- tests/lib/cleanup.py | 4 +- tests/unit/test_controller.py | 4 +- tests/unit/test_daemon.py | 12 +- tests/unit/test_data_manager.py | 4 +- .../test_event_pipeline.py | 64 +++---- tests/unit/test_event_reader.py | 8 +- tests/unit/test_injector.py | 24 +-- tests/unit/test_macros.py | 172 +++++++++--------- tests/unit/test_mapping.py | 8 +- tests/unit/test_system_mapping.py | 92 +++++----- 26 files changed, 241 insertions(+), 242 deletions(-) rename inputremapper/configs/{system_mapping.py => keyboard_layout.py} (97%) diff --git a/bin/input-remapper-control b/bin/input-remapper-control index 33659904a..438fb2d74 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -90,9 +90,9 @@ class InputRemapperControl: print(group.key) def list_key_names(self): - from inputremapper.configs.system_mapping import system_mapping + from inputremapper.configs.keyboard_layout import keyboard_layout - print("\n".join(system_mapping.list_names())) + print("\n".join(keyboard_layout.list_names())) def communicate( self, diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index 77a30b4d3..68cd9cf48 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -74,7 +74,7 @@ if __name__ == "__main__": # log-level is set. # import input-remapper stuff after setting the log verbosity from inputremapper.gui.messages.message_broker import MessageBroker, MessageType - from inputremapper.configs.system_mapping import system_mapping + from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.gui.data_manager import DataManager from inputremapper.gui.user_interface import UserInterface from inputremapper.gui.controller import Controller @@ -105,7 +105,7 @@ if __name__ == "__main__": reader_client, daemon, global_uinputs, - system_mapping, + keyboard_layout, ) controller = Controller(message_broker, data_manager) user_interface = UserInterface(message_broker, controller) diff --git a/inputremapper/configs/input_config.py b/inputremapper/configs/input_config.py index 5648859a7..eaf020a3c 100644 --- a/inputremapper/configs/input_config.py +++ b/inputremapper/configs/input_config.py @@ -31,7 +31,7 @@ except ImportError: from pydantic import BaseModel, root_validator, validator -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.gui.messages.message_types import MessageType from inputremapper.logging.logger import logger from inputremapper.utils import get_evdev_constant_name @@ -142,7 +142,7 @@ def _get_name(self) -> Optional[str]: # first try to find the name in xmodmap to not display wrong # names due to the keyboard layout if self.type == ecodes.EV_KEY: - key_name = system_mapping.get_name(self.code) + key_name = keyboard_layout.get_name(self.code) if key_name is None: # if no result, look in the linux combination constants. On a german diff --git a/inputremapper/configs/system_mapping.py b/inputremapper/configs/keyboard_layout.py similarity index 97% rename from inputremapper/configs/system_mapping.py rename to inputremapper/configs/keyboard_layout.py index f88493b3a..0cc91220d 100644 --- a/inputremapper/configs/system_mapping.py +++ b/inputremapper/configs/keyboard_layout.py @@ -41,7 +41,7 @@ LAZY_LOAD = None -class SystemMapping: +class KeyboardLayout: """Stores information about all available keycodes.""" _mapping: Optional[dict] = LAZY_LOAD @@ -49,7 +49,7 @@ class SystemMapping: _case_insensitive_mapping: Optional[dict] = LAZY_LOAD def __getattribute__(self, wanted: str): - """To lazy load system_mapping info only when needed. + """To lazy load keyboard_layout info only when needed. For example, this helps to keep logs of input-remapper-control clear when it doesn't need it the information. @@ -160,7 +160,7 @@ def _set(self, name: str, code: int): def get(self, name: str) -> int: """Return the code mapped to the key.""" - # the correct casing should be shown when asking the system_mapping + # the correct casing should be shown when asking the keyboard_layout # for stuff. indexing case insensitive to support old presets. if name not in self._mapping: # only if not e.g. both "a" and "A" are in the mapping @@ -216,4 +216,4 @@ def _find_legit_mappings(self) -> dict: # TODO DI # this mapping represents the xmodmap output, which stays constant -system_mapping = SystemMapping() +keyboard_layout = KeyboardLayout() diff --git a/inputremapper/configs/mapping.py b/inputremapper/configs/mapping.py index c1e09b33b..90aac9ad7 100644 --- a/inputremapper/configs/mapping.py +++ b/inputremapper/configs/mapping.py @@ -62,7 +62,7 @@ ) from inputremapper.configs.input_config import InputCombination -from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME +from inputremapper.configs.keyboard_layout import keyboard_layout, DISABLE_NAME from inputremapper.configs.validation_errors import ( OutputSymbolUnknownError, SymbolNotAvailableInTargetError, @@ -288,13 +288,13 @@ def remove_combination_changed_callback(self): def get_output_type_code(self) -> Optional[Tuple[int, int]]: """Returns the output_type and output_code if set, - otherwise looks the output_symbol up in the system_mapping + otherwise looks the output_symbol up in the keyboard_layout return None for unknown symbols and macros """ if self.output_code and self.output_type: return self.output_type, self.output_code if self.output_symbol and not is_this_a_macro(self.output_symbol): - return EV_KEY, system_mapping.get(self.output_symbol) + return EV_KEY, keyboard_layout.get(self.output_symbol) return None def get_output_name_constant(self) -> str: @@ -387,7 +387,7 @@ def validate_symbol(cls, values): parse(symbol, mapping=mapping_mock, verbose=False) return values - code = system_mapping.get(symbol) + code = keyboard_layout.get(symbol) if code is None: raise OutputSymbolUnknownError(symbol) @@ -452,7 +452,7 @@ def validate_output_integrity(cls, values): if type_ is not None or code is not None: raise MacroButTypeOrCodeSetError() - if code is not None and code != system_mapping.get(symbol) or type_ != EV_KEY: + if code is not None and code != keyboard_layout.get(symbol) or type_ != EV_KEY: raise SymbolAndCodeMismatchError(symbol, code) return values diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index 2131a1826..14bdf5fbe 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -51,7 +51,7 @@ from inputremapper.configs.mapping import Mapping, UIMapping from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.macros.parse import is_this_a_macro from inputremapper.logging.logger import logger, VERSION @@ -230,7 +230,7 @@ def _find_target(self, symbol): # capabilities = parse(symbol).get_capabilities() return None - capabilities[EV_KEY] = {system_mapping.get(symbol)} + capabilities[EV_KEY] = {keyboard_layout.get(symbol)} if len(capabilities[EV_REL]) > 0: return "mouse" diff --git a/inputremapper/configs/validation_errors.py b/inputremapper/configs/validation_errors.py index 156a7c980..e1c163959 100644 --- a/inputremapper/configs/validation_errors.py +++ b/inputremapper/configs/validation_errors.py @@ -31,7 +31,7 @@ from evdev.ecodes import EV_KEY -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.injection.global_uinputs import GlobalUInputs @@ -62,7 +62,7 @@ def __init__(self, analog_events): class SymbolNotAvailableInTargetError(ValueError): def __init__(self, symbol, target): - code = system_mapping.get(symbol) + code = keyboard_layout.get(symbol) fitting_targets = GlobalUInputs.find_fitting_default_uinputs(EV_KEY, code) fitting_targets_string = '", "'.join(fitting_targets) @@ -92,8 +92,8 @@ class SymbolAndCodeMismatchError(ValueError): def __init__(self, symbol, code): super().__init__( "output_symbol and output_code mismatch: " - f"output macro is {symbol} -> {system_mapping.get(symbol)} " - f"but output_code is {code} -> {system_mapping.get_name(code)} " + f"output macro is {symbol} -> {keyboard_layout.get(symbol)} " + f"but output_code is {code} -> {keyboard_layout.get_name(code)} " ) diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index b088cb2f8..e63095347 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -44,7 +44,7 @@ from inputremapper.injection.injector import Injector, InjectorState from inputremapper.configs.preset import Preset from inputremapper.configs.global_config import GlobalConfig -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.groups import groups from inputremapper.configs.paths import PathUtils from inputremapper.user import UserUtils @@ -487,13 +487,13 @@ def start_injecting(self, group_key: str, preset_name: str) -> bool: xmodmap = json.load(file) logger.debug('Using keycodes from "%s"', xmodmap_path) - # this creates the system_mapping._xmodmap, which we need to do now + # this creates the keyboard_layout._xmodmap, which we need to do now # otherwise it might be created later which will override the changes # we do here. - # Do we really need to lazyload in the system_mapping? + # Do we really need to lazyload in the keyboard_layout? # this kind of bug is stupid to track down - system_mapping.get_name(0) - system_mapping.update(xmodmap) + keyboard_layout.get_name(0) + keyboard_layout.update(xmodmap) # the service now has process wide knowledge of xmodmap # keys of the users session except FileNotFoundError: diff --git a/inputremapper/gui/autocompletion.py b/inputremapper/gui/autocompletion.py index 455be58cb..28bb6f4fd 100644 --- a/inputremapper/gui/autocompletion.py +++ b/inputremapper/gui/autocompletion.py @@ -28,7 +28,7 @@ from gi.repository import Gdk, Gtk, GLib, GObject from inputremapper.configs.mapping import MappingData -from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME +from inputremapper.configs.keyboard_layout import keyboard_layout, DISABLE_NAME from inputremapper.gui.components.editor import CodeEditor from inputremapper.gui.controller import Controller from inputremapper.gui.messages.message_broker import MessageBroker, MessageType @@ -111,7 +111,7 @@ def propose_symbols(text_iter: Gtk.TextIter, codes: List[int]) -> List[Tuple[str incomplete_name = incomplete_name.lower() - names = list(system_mapping.list_names(codes=codes)) + [DISABLE_NAME] + names = list(keyboard_layout.list_names(codes=codes)) + [DISABLE_NAME] return [ (name, name) diff --git a/inputremapper/gui/components/editor.py b/inputremapper/gui/components/editor.py index f250a42f4..80ec4818d 100644 --- a/inputremapper/gui/components/editor.py +++ b/inputremapper/gui/components/editor.py @@ -57,7 +57,7 @@ from inputremapper.gui.utils import HandlerDisabled, Colors from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.input_event import InputEvent -from inputremapper.configs.system_mapping import system_mapping, XKB_KEYCODE_OFFSET +from inputremapper.configs.keyboard_layout import keyboard_layout, XKB_KEYCODE_OFFSET from inputremapper.utils import get_evdev_constant_name Capabilities = Dict[int, List] @@ -384,9 +384,9 @@ def _display(self, event): if is_press and len(self._combination) > 0: names = [ - system_mapping.get_name(code) + keyboard_layout.get_name(code) for code in self._combination - if code is not None and system_mapping.get_name(code) is not None + if code is not None and keyboard_layout.get_name(code) is not None ] self._gui.set_text(" + ".join(names)) diff --git a/inputremapper/gui/data_manager.py b/inputremapper/gui/data_manager.py index f4d89feae..69577f12b 100644 --- a/inputremapper/gui/data_manager.py +++ b/inputremapper/gui/data_manager.py @@ -30,7 +30,7 @@ from inputremapper.configs.mapping import UIMapping, MappingData from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import SystemMapping +from inputremapper.configs.keyboard_layout import KeyboardLayout from inputremapper.daemon import DaemonProxy from inputremapper.exceptions import DataManagementError from inputremapper.groups import _Group @@ -73,13 +73,13 @@ def __init__( reader_client: ReaderClient, daemon: DaemonProxy, uinputs: GlobalUInputs, - system_mapping: SystemMapping, + keyboard_layout: KeyboardLayout, ): self.message_broker = message_broker self._reader_client = reader_client self._daemon = daemon self._uinputs = uinputs - self._system_mapping = system_mapping + self._keyboard_layout = keyboard_layout uinputs.prepare_all() self._config = config @@ -454,7 +454,7 @@ def update_mapping(self, **kwargs): raise DataManagementError("Cannot modify Mapping: Mapping is not set") if symbol := kwargs.get("output_symbol"): - kwargs["output_symbol"] = self._system_mapping.correct_case(symbol) + kwargs["output_symbol"] = self._keyboard_layout.correct_case(symbol) combination = self.active_mapping.input_combination for key, value in kwargs.items(): diff --git a/inputremapper/gui/utils.py b/inputremapper/gui/utils.py index 15122514d..8da31547c 100644 --- a/inputremapper/gui/utils.py +++ b/inputremapper/gui/utils.py @@ -145,9 +145,6 @@ def run_all_now(self): logger.error(exception) -debounce_manager = DebounceManager() - - def debounce(timeout): """Debounce a method call to improve performance. @@ -176,6 +173,8 @@ def wrapped(self, *args, **kwargs): return decorator +debounce_manager = DebounceManager() + class HandlerDisabled: """Safely modify a widget without causing handlers to be called. diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index b0d3fd0e9..0876e7d7d 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -54,7 +54,7 @@ REL_HWHEEL, ) -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.configs.validation_errors import ( SymbolNotAvailableInTargetError, MacroParsingError, @@ -728,7 +728,7 @@ def _type_check_symbol(self, keyname: Union[str, Variable]) -> Union[Variable, i return keyname symbol = str(keyname) - code = system_mapping.get(symbol) + code = keyboard_layout.get(symbol) if code is None: raise MacroParsingError(msg=f'Unknown key "{symbol}"') diff --git a/inputremapper/injection/mapping_handlers/mapping_parser.py b/inputremapper/injection/mapping_handlers/mapping_parser.py index 9f2343002..31477ab33 100644 --- a/inputremapper/injection/mapping_handlers/mapping_parser.py +++ b/inputremapper/injection/mapping_handlers/mapping_parser.py @@ -27,7 +27,7 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import DISABLE_CODE, DISABLE_NAME +from inputremapper.configs.keyboard_layout import DISABLE_CODE, DISABLE_NAME from inputremapper.exceptions import MappingParsingError from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.macros.parse import is_this_a_macro diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 6d1f23b70..37c1a67ad 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -37,7 +37,7 @@ from tests.lib.logger import logger from inputremapper.gui.controller import Controller -from inputremapper.configs.system_mapping import XKB_KEYCODE_OFFSET +from inputremapper.configs.keyboard_layout import XKB_KEYCODE_OFFSET from inputremapper.gui.utils import CTX_ERROR, CTX_WARNING, gtk_iteration from inputremapper.gui.messages.message_broker import ( MessageBroker, diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index 89e790592..9aa395283 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -65,7 +65,7 @@ gi.require_version("GLib", "2.0") from gi.repository import Gtk, GLib, Gdk, GtkSource -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.configs.mapping import Mapping from inputremapper.configs.paths import PathUtils from inputremapper.configs.global_config import GlobalConfig @@ -2156,9 +2156,9 @@ def test_autocomplete_key(self): complete_key_name = "Test_Foo_Bar" - system_mapping.clear() - system_mapping._set(complete_key_name, 1) - system_mapping._set("KEY_A", 30) # we need this for the UIMapping to work + keyboard_layout.clear() + keyboard_layout._set(complete_key_name, 1) + keyboard_layout._set("KEY_A", 30) # we need this for the UIMapping to work # it can autocomplete a combination inbetween other things incomplete = "qux_1\n + + qux_2" diff --git a/tests/lib/cleanup.py b/tests/lib/cleanup.py index 18ffc7515..daee127ee 100644 --- a/tests/lib/cleanup.py +++ b/tests/lib/cleanup.py @@ -82,7 +82,7 @@ def quick_cleanup(log=True): # Reminder: before patches are applied in test.py, no inputremapper module # may be imported. So tests.lib imports them just-in-time in functions instead. from inputremapper.injection.macros.macro import macro_variables - from inputremapper.configs.system_mapping import system_mapping + from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.gui.utils import debounce_manager from inputremapper.injection.global_uinputs import GlobalUInputs @@ -128,7 +128,7 @@ def quick_cleanup(log=True): if os.path.exists(tmp): shutil.rmtree(tmp) - system_mapping.populate() + keyboard_layout.populate() clear_write_history() diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index 66e387525..f28aab23c 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -26,7 +26,7 @@ import gi from evdev.ecodes import EV_ABS, ABS_X, ABS_Y, ABS_RX -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.injection.injector import InjectorState gi.require_version("Gtk", "3.0") @@ -76,7 +76,7 @@ def setUp(self) -> None: ReaderClient(self.message_broker, _Groups()), FakeDaemonProxy(), uinputs, - system_mapping, + keyboard_layout, ) self.user_interface = MagicMock() self.controller = Controller(self.message_broker, self.data_manager) diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index 3eb77dace..6e0c0529b 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -34,7 +34,7 @@ from inputremapper.configs.mapping import Mapping from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.daemon import Daemon from inputremapper.groups import groups from inputremapper.injection.global_uinputs import GlobalUInputs, UInput @@ -236,8 +236,8 @@ def test_refresh_on_start(self): # this test only makes sense if this device is unknown yet self.assertIsNone(group) - system_mapping.clear() - system_mapping._set("a", KEY_A) + keyboard_layout.clear() + keyboard_layout._set("a", KEY_A) preset = Preset(PathUtils.get_preset_path(group_name, preset_name)) preset.add( @@ -250,8 +250,8 @@ def test_refresh_on_start(self): # make the daemon load the file instead with open(PathUtils.get_config_path("xmodmap.json"), "w") as file: - json.dump(system_mapping._mapping, file, indent=4) - system_mapping.clear() + json.dump(keyboard_layout._mapping, file, indent=4) + keyboard_layout.clear() preset.save() self.global_config.set_autoload_preset(group_key, preset_name) @@ -346,7 +346,7 @@ def test_xmodmap_file(self): ) preset.save() - system_mapping.clear() + keyboard_layout.clear() push_events( fixtures.bar_device, diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index 5f07fba0a..770c7caad 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -30,7 +30,7 @@ from inputremapper.configs.mapping import UIMapping, MappingData from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.exceptions import DataManagementError from inputremapper.groups import _Groups from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME @@ -71,7 +71,7 @@ def setUp(self) -> None: self.reader, FakeDaemonProxy(), self.uinputs, - system_mapping, + keyboard_layout, ) def test_load_group_provides_presets(self): diff --git a/tests/unit/test_event_pipeline/test_event_pipeline.py b/tests/unit/test_event_pipeline/test_event_pipeline.py index 2b2b10ae6..bd542d9e5 100644 --- a/tests/unit/test_event_pipeline/test_event_pipeline.py +++ b/tests/unit/test_event_pipeline/test_event_pipeline.py @@ -54,7 +54,7 @@ DEFAULT_REL_RATE, ) from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader @@ -141,19 +141,19 @@ async def test_any_event_as_button(self): c_up = (EV_ABS, ABS_HAT0X, 0) # first change the system mapping because Mapping will validate against it - system_mapping.clear() + keyboard_layout.clear() code_w = 71 code_b = 72 code_c = 73 code_d = 74 code_a = 75 code_s = 76 - system_mapping._set("w", code_w) - system_mapping._set("d", code_d) - system_mapping._set("a", code_a) - system_mapping._set("s", code_s) - system_mapping._set("b", code_b) - system_mapping._set("c", code_c) + keyboard_layout._set("w", code_w) + keyboard_layout._set("d", code_d) + keyboard_layout._set("a", code_a) + keyboard_layout._set("s", code_s) + keyboard_layout._set("b", code_b) + keyboard_layout._set("c", code_c) preset = Preset() preset.add( @@ -256,10 +256,10 @@ async def test_reset_releases_keys(self): ) event_reader = self.create_event_reader(preset, fixtures.foo_device_2_keyboard) - a = system_mapping.get("a") - b = system_mapping.get("b") - c = system_mapping.get("c") - d = system_mapping.get("d") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") + c = keyboard_layout.get("c") + d = keyboard_layout.get("d") await self.send_events( [ @@ -297,7 +297,7 @@ async def test_forward_abs(self): """Test if EV_ABS events are forwarded when other events of the same input are not.""" preset = Preset() # BTN_A -> 77 - system_mapping._set("b", 77) + keyboard_layout._set("b", 77) preset.add( Mapping.from_combination( InputCombination([InputConfig(type=EV_KEY, code=BTN_A)]), @@ -335,7 +335,7 @@ async def test_forward_rel(self): """Test if EV_REL events are forwarded when other events of the same input are not.""" preset = Preset() # BTN_A -> 77 - system_mapping._set("b", 77) + keyboard_layout._set("b", 77) preset.add( Mapping.from_combination( InputCombination([InputConfig(type=EV_KEY, code=BTN_LEFT)]), @@ -372,9 +372,9 @@ async def test_forward_rel(self): async def test_combination(self): """Test if combinations map to keys properly.""" - a = system_mapping.get("a") - b = system_mapping.get("b") - c = system_mapping.get("c") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") + c = keyboard_layout.get("c") origin = fixtures.gamepad origin_hash = origin.get_device_hash() @@ -491,7 +491,7 @@ async def test_ignore_hold(self): output_symbol="a", ) ) - a = system_mapping.get("a") + a = keyboard_layout.get("a") event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( @@ -565,9 +565,9 @@ async def test_ignore_disabled(self): ) ) - a = system_mapping.get("a") - b = system_mapping.get("b") - c = system_mapping.get("c") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") + c = keyboard_layout.get("c") event_reader = self.create_event_reader(preset, origin) @@ -639,8 +639,8 @@ async def test_combination_keycode_macro_mix(self): up_1 = (EV_ABS, ABS_HAT0X, 0) up_2 = (EV_ABS, ABS_HAT0Y, 0) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") preset = Preset() preset.add( @@ -722,8 +722,8 @@ async def test_wheel_combination_release_failure(self): InputCombination.from_tuples((1, 276, 1), (2, 8, -1)) ) - system_mapping.clear() - system_mapping._set("a", 30) + keyboard_layout.clear() + keyboard_layout._set("a", 30) a = 30 m = Mapping.from_combination(combination, output_symbol="a") @@ -1337,11 +1337,11 @@ async def test_rel_to_btn(self): # should be forwarded and present in the capabilities hw_left = (EV_REL, REL_HWHEEL, -1) - system_mapping.clear() + keyboard_layout.clear() code_b = 91 code_c = 92 - system_mapping._set("b", code_b) - system_mapping._set("c", code_c) + keyboard_layout._set("b", code_b) + keyboard_layout._set("c", code_c) # set a high release timeout to make sure the tests pass release_timeout = 0.2 @@ -1412,8 +1412,8 @@ async def test_rel_trigger_threshold(self): preset.add(mapping_1) preset.add(mapping_2) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") event_reader = self.create_event_reader(preset, fixtures.foo_device_2_mouse) @@ -1481,8 +1481,8 @@ async def test_abs_trigger_threshold(self): preset.add(mapping_1) preset.add(mapping_2) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") event_reader = self.create_event_reader(preset, fixtures.gamepad) diff --git a/tests/unit/test_event_reader.py b/tests/unit/test_event_reader.py index bef1bb58f..cbbae18d9 100644 --- a/tests/unit/test_event_reader.py +++ b/tests/unit/test_event_reader.py @@ -39,7 +39,7 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader from inputremapper.injection.global_uinputs import GlobalUInputs, UInput @@ -75,8 +75,8 @@ async def test_if_single_joystick_then(self): # TODO: Move this somewhere more sensible # Integration test style for if_single. # won't care about the event, because the purpose is not set to BUTTON - code_a = system_mapping.get("a") - code_shift = system_mapping.get("KEY_LEFTSHIFT") + code_a = keyboard_layout.get("a") + code_shift = keyboard_layout.get("KEY_LEFTSHIFT") trigger = evdev.ecodes.BTN_A self.preset.add( @@ -189,7 +189,7 @@ async def test_if_single_joystick_then(self): async def test_if_single_joystick_under_threshold(self): """Triggers then because the joystick events value is too low.""" # TODO: Move this somewhere more sensible - code_a = system_mapping.get("a") + code_a = keyboard_layout.get("a") trigger = evdev.ecodes.BTN_A self.preset.add( Mapping.from_combination( diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index 736a9c159..7fc2fa921 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -45,8 +45,8 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import ( - system_mapping, +from inputremapper.configs.keyboard_layout import ( + keyboard_layout, DISABLE_CODE, DISABLE_NAME, ) @@ -374,13 +374,13 @@ def test_injector(self): numlock_before = is_numlock_on() # stuff the preset outputs - system_mapping.clear() + keyboard_layout.clear() code_a = 100 code_q = 101 code_w = 102 - system_mapping._set("a", code_a) - system_mapping._set("key_q", code_q) - system_mapping._set("w", code_w) + keyboard_layout._set("a", code_a) + keyboard_layout._set("key_q", code_q) + keyboard_layout._set("w", code_w) preset = Preset() preset.add( @@ -419,7 +419,7 @@ def test_injector(self): "a", ) ) - # one mapping that is unknown in the system_mapping on purpose + # one mapping that is unknown in the keyboard_layout on purpose input_b = 10 with self.assertRaises(ValidationError): preset.add( @@ -648,11 +648,11 @@ def capabilities(self, absinfo=False): ), ) - self.a = system_mapping.get("a") - self.shift_l = system_mapping.get("ShIfT_L") - self.one = system_mapping.get(1) - self.two = system_mapping.get("2") - self.left = system_mapping.get("BtN_lEfT") + self.a = keyboard_layout.get("a") + self.shift_l = keyboard_layout.get("ShIfT_L") + self.one = keyboard_layout.get(1) + self.two = keyboard_layout.get("2") + self.left = keyboard_layout.get("BtN_lEfT") self.fake_device = FakeDevice() self.preset = preset self.macro = macro diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index 3dcfb55d2..92c3f7915 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -40,7 +40,7 @@ ) from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.configs.validation_errors import ( MacroParsingError, SymbolNotAvailableInTargetError, @@ -355,19 +355,19 @@ async def test_run_plus_syntax(self): self.assertTrue(macro.is_holding()) # starting from the left, presses each one down - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("a"), 1)) - self.assertEqual(self.result[1], (EV_KEY, system_mapping.get("b"), 1)) - self.assertEqual(self.result[2], (EV_KEY, system_mapping.get("c"), 1)) - self.assertEqual(self.result[3], (EV_KEY, system_mapping.get("d"), 1)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("a"), 1)) + self.assertEqual(self.result[1], (EV_KEY, keyboard_layout.get("b"), 1)) + self.assertEqual(self.result[2], (EV_KEY, keyboard_layout.get("c"), 1)) + self.assertEqual(self.result[3], (EV_KEY, keyboard_layout.get("d"), 1)) # and then releases starting with the previously pressed key macro.release_trigger() await asyncio.sleep(0.2) self.assertFalse(macro.is_holding()) - self.assertEqual(self.result[4], (EV_KEY, system_mapping.get("d"), 0)) - self.assertEqual(self.result[5], (EV_KEY, system_mapping.get("c"), 0)) - self.assertEqual(self.result[6], (EV_KEY, system_mapping.get("b"), 0)) - self.assertEqual(self.result[7], (EV_KEY, system_mapping.get("a"), 0)) + self.assertEqual(self.result[4], (EV_KEY, keyboard_layout.get("d"), 0)) + self.assertEqual(self.result[5], (EV_KEY, keyboard_layout.get("c"), 0)) + self.assertEqual(self.result[6], (EV_KEY, keyboard_layout.get("b"), 0)) + self.assertEqual(self.result[7], (EV_KEY, keyboard_layout.get("a"), 0)) async def test_extract_params(self): # splits strings, doesn't try to understand their meaning yet @@ -447,7 +447,7 @@ async def test_parse_params(self): async def test_0(self): macro = parse("key(1)", self.context, DummyMapping, True) - one_code = system_mapping.get("1") + one_code = keyboard_layout.get("1") await macro.run(self.handler) self.assertListEqual( @@ -463,12 +463,12 @@ async def test_1(self): self.assertListEqual( self.result, [ - (EV_KEY, system_mapping.get("1"), 1), - (EV_KEY, system_mapping.get("1"), 0), - (EV_KEY, system_mapping.get("a"), 1), - (EV_KEY, system_mapping.get("a"), 0), - (EV_KEY, system_mapping.get("3"), 1), - (EV_KEY, system_mapping.get("3"), 0), + (EV_KEY, keyboard_layout.get("1"), 1), + (EV_KEY, keyboard_layout.get("1"), 0), + (EV_KEY, keyboard_layout.get("a"), 1), + (EV_KEY, keyboard_layout.get("a"), 0), + (EV_KEY, keyboard_layout.get("3"), 1), + (EV_KEY, keyboard_layout.get("3"), 0), ], ) self.assertEqual(len(macro.child_macros), 0) @@ -569,8 +569,8 @@ async def test_raises_error(self): ) async def test_key(self): - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") macro = parse("set(foo, b).key($foo).key(a)", self.context, DummyMapping) await macro.run(self.handler) self.assertListEqual( @@ -584,8 +584,8 @@ async def test_key(self): ) async def test_key_down_up(self): - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") macro = parse( "set(foo, b).key_down($foo).key_up($foo).key_up(a).key_down(a)", self.context, @@ -603,9 +603,9 @@ async def test_key_down_up(self): ) async def test_modify(self): - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") - code_c = system_mapping.get("c") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") + code_c = keyboard_layout.get("c") macro = parse( "set(foo, b).modify($foo, modify(a, key(c)))", self.context, @@ -625,7 +625,7 @@ async def test_modify(self): ) async def test_hold_variable(self): - code_a = system_mapping.get("a") + code_a = keyboard_layout.get("a") macro = parse("set(foo, a).hold($foo)", self.context, DummyMapping) await macro.run(self.handler) self.assertListEqual( @@ -643,9 +643,9 @@ async def test_hold_keys(self): # then run, just like how it is going to happen during runtime asyncio.ensure_future(macro.run(self.handler)) - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") - code_c = system_mapping.get("c") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") + code_c = keyboard_layout.get("c") await asyncio.sleep(0.2) self.assertListEqual( @@ -694,10 +694,10 @@ async def test_hold(self): await asyncio.sleep(0.05) self.assertFalse(macro.is_holding()) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("1"), 1)) - self.assertEqual(self.result[-1], (EV_KEY, system_mapping.get("3"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("1"), 1)) + self.assertEqual(self.result[-1], (EV_KEY, keyboard_layout.get("3"), 0)) - code_a = system_mapping.get("a") + code_a = keyboard_layout.get("a") self.assertGreater(self.result.count((EV_KEY, code_a, 1)), 2) self.assertEqual(len(macro.child_macros), 1) @@ -731,8 +731,8 @@ async def test_dont_hold(self): # and the child macro of hold is never called. self.assertEqual(len(self.result), 4) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("1"), 1)) - self.assertEqual(self.result[-1], (EV_KEY, system_mapping.get("3"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("1"), 1)) + self.assertEqual(self.result[-1], (EV_KEY, keyboard_layout.get("3"), 0)) self.assertEqual(len(macro.child_macros), 1) @@ -757,8 +757,8 @@ async def test_just_hold(self): self.assertFalse(macro.is_holding()) self.assertEqual(len(self.result), 4) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("1"), 1)) - self.assertEqual(self.result[-1], (EV_KEY, system_mapping.get("3"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("1"), 1)) + self.assertEqual(self.result[-1], (EV_KEY, keyboard_layout.get("3"), 0)) self.assertEqual(len(macro.child_macros), 0) @@ -772,8 +772,8 @@ async def test_dont_just_hold(self): # completely self.assertEqual(len(self.result), 4) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("1"), 1)) - self.assertEqual(self.result[-1], (EV_KEY, system_mapping.get("3"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("1"), 1)) + self.assertEqual(self.result[-1], (EV_KEY, keyboard_layout.get("3"), 0)) self.assertEqual(len(macro.child_macros), 0) @@ -793,7 +793,7 @@ async def test_hold_down(self): await asyncio.sleep(0.2) self.assertTrue(macro.is_holding()) self.assertEqual(len(self.result), 1) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("a"), 1)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("a"), 1)) """up""" @@ -802,8 +802,8 @@ async def test_hold_down(self): self.assertFalse(macro.is_holding()) self.assertEqual(len(self.result), 2) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("a"), 1)) - self.assertEqual(self.result[1], (EV_KEY, system_mapping.get("a"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("a"), 1)) + self.assertEqual(self.result[1], (EV_KEY, keyboard_layout.get("a"), 0)) async def test_2(self): start = time.time() @@ -814,7 +814,7 @@ async def test_2(self): self.context, DummyMapping, ) - k_code = system_mapping.get("k") + k_code = keyboard_layout.get("k") await macro.run(self.handler) keystroke_sleep = DummyMapping.macro_key_sleep_ms @@ -833,7 +833,7 @@ async def test_2(self): async def test_3(self): start = time.time() macro = parse("repeat(3, key(m).w(100))", self.context, DummyMapping) - m_code = system_mapping.get("m") + m_code = keyboard_layout.get("m") await macro.run(self.handler) keystroke_time = 6 * DummyMapping.macro_key_sleep_ms @@ -863,9 +863,9 @@ async def test_4(self): DummyMapping, ) - r = system_mapping.get("r") - minus = system_mapping.get("minus") - m = system_mapping.get("m") + r = keyboard_layout.get("r") + minus = keyboard_layout.get("minus") + m = keyboard_layout.get("m") await macro.run(self.handler) self.assertListEqual( @@ -897,9 +897,9 @@ async def test_5(self): self.assertEqual(len(macro.child_macros), 1) self.assertEqual(len(macro.child_macros[0].child_macros), 1) - w = system_mapping.get("w") - left = system_mapping.get("bTn_lEfT") - k = system_mapping.get("k") + w = keyboard_layout.get("w") + left = keyboard_layout.get("bTn_lEfT") + k = keyboard_layout.get("k") await macro.run(self.handler) @@ -928,9 +928,9 @@ async def test_duplicate_run(self): # internal state (in particular the _trigger_release_event). # I actually don't know at all what kind of bugs that might produce, # lets just avoid it. It might cause it to be held down forever. - a = system_mapping.get("a") - b = system_mapping.get("b") - c = system_mapping.get("c") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") + c = keyboard_layout.get("c") macro = parse("key(a).modify(b, hold()).key(c)", self.context, DummyMapping) asyncio.ensure_future(macro.run(self.handler)) @@ -1036,7 +1036,7 @@ async def test_mouse_accel(self): async def test_event_1(self): macro = parse("e(EV_KEY, KEY_A, 1)", self.context, DummyMapping) - a_code = system_mapping.get("a") + a_code = keyboard_layout.get("a") await macro.run(self.handler) self.assertListEqual(self.result, [(EV_KEY, a_code, 1)]) @@ -1169,15 +1169,15 @@ async def test_ifeq_runs(self): self.context, DummyMapping, ) - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") await macro.run(self.handler) self.assertListEqual(self.result, [(EV_KEY, code_a, 1), (EV_KEY, code_a, 0)]) self.assertEqual(len(macro.child_macros), 2) async def test_ifeq_none(self): - code_a = system_mapping.get("a") + code_a = keyboard_layout.get("a") # first param None macro = parse( @@ -1214,8 +1214,8 @@ async def test_ifeq_none(self): async def test_ifeq_unknown_key(self): macro = parse("ifeq(qux, 2, key(a), key(b))", self.context, DummyMapping) - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") await macro.run(self.handler) self.assertListEqual(self.result, [(EV_KEY, code_b, 1), (EV_KEY, code_b, 0)]) @@ -1223,8 +1223,8 @@ async def test_ifeq_unknown_key(self): async def test_if_eq(self): """new version of ifeq""" - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") a_press = [(EV_KEY, code_a, 1), (EV_KEY, code_a, 0)] b_press = [(EV_KEY, code_b, 1), (EV_KEY, code_b, 0)] @@ -1277,8 +1277,8 @@ async def test(macro, expected): async def test_if_eq_runs_multiprocessed(self): """ifeq on variables that have been set in other processes works.""" macro = parse("if_eq($foo, 3, key(a), key(b))", self.context, DummyMapping) - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") self.assertEqual(len(macro.child_macros), 2) @@ -1319,10 +1319,10 @@ async def test_if_single(self): macro = parse("if_single(key(x), key(y))", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 2) - a = system_mapping.get("a") + a = keyboard_layout.get("a") - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") await self.trigger_sequence(macro, InputEvent.key(a, 1)) await asyncio.sleep(0.1) @@ -1343,11 +1343,11 @@ async def test_if_single_ignores_releases(self): ) self.assertEqual(len(macro.child_macros), 2) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") # pressing the macro key await self.trigger_sequence(macro, InputEvent.key(a, 1)) @@ -1381,11 +1381,11 @@ async def test_if_not_single(self): self.assertEqual(len(macro.child_macros), 1) self.assertEqual(len(macro.child_macros[0].child_macros), 2) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") # press the trigger key await self.trigger_sequence(macro, InputEvent.key(a, 1)) @@ -1402,10 +1402,10 @@ async def test_if_not_single_none(self): macro = parse("if_single(key(x),)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 1) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") - x = system_mapping.get("x") + x = keyboard_layout.get("x") # press trigger key await self.trigger_sequence(macro, InputEvent.key(a, 1)) @@ -1426,8 +1426,8 @@ async def test_if_single_times_out(self): ) self.assertEqual(len(macro.child_macros), 2) - a = system_mapping.get("a") - y = system_mapping.get("y") + a = keyboard_layout.get("a") + y = keyboard_layout.get("y") await self.trigger_sequence(macro, InputEvent.key(a, 1)) @@ -1446,8 +1446,8 @@ async def test_if_single_ignores_joystick(self): # Integration test style for if_single. # If a joystick that is mapped to a button is moved, if_single stops macro = parse("if_single(k(a), k(KEY_LEFTSHIFT))", self.context, DummyMapping) - code_shift = system_mapping.get("KEY_LEFTSHIFT") - code_a = system_mapping.get("a") + code_shift = keyboard_layout.get("KEY_LEFTSHIFT") + code_a = keyboard_layout.get("a") trigger = 1 await self.trigger_sequence(macro, InputEvent.key(trigger, 1)) @@ -1467,8 +1467,8 @@ async def test_if_tap(self): macro = parse("if_tap(key(x), key(y), 100)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 2) - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") # this is the regular routine of how a macro is started. the tigger is pressed # already when the macro runs, and released during if_tap within the timeout. @@ -1548,7 +1548,7 @@ async def test_if_tap_none(self): # first param none macro = parse("if_tap(, key(y), 100)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 1) - y = system_mapping.get("y") + y = keyboard_layout.get("y") macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) await asyncio.sleep(0.05) @@ -1559,7 +1559,7 @@ async def test_if_tap_none(self): # second param none macro = parse("if_tap(key(y), , 50)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 1) - y = system_mapping.get("y") + y = keyboard_layout.get("y") macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) await asyncio.sleep(0.1) @@ -1573,8 +1573,8 @@ async def test_if_not_tap(self): macro = parse("if_tap(key(x), key(y), 50)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 2) - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) @@ -1589,8 +1589,8 @@ async def test_if_not_tap_named(self): macro = parse("if_tap(key(x), key(y), timeout=50)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 2) - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py index 283747c10..372f4e873 100644 --- a/tests/unit/test_mapping.py +++ b/tests/unit/test_mapping.py @@ -37,7 +37,7 @@ from pydantic import ValidationError from inputremapper.configs.mapping import Mapping, UIMapping -from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME +from inputremapper.configs.keyboard_layout import keyboard_layout, DISABLE_NAME from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.gui.messages.message_broker import MessageType from tests.lib.test_setup import test_setup @@ -105,7 +105,7 @@ def test_get_output_type_code(self): "output_symbol": "a", } m = Mapping(**cfg) - a = system_mapping.get("a") + a = keyboard_layout.get("a") self.assertEqual(m.get_output_type_code(), (EV_KEY, a)) m.output_symbol = "key(a)" @@ -127,7 +127,7 @@ def test_strips_output_symbol(self): "output_symbol": "\t a \n", } m = Mapping(**cfg) - a = system_mapping.get("a") + a = keyboard_layout.get("a") self.assertEqual(m.get_output_type_code(), (EV_KEY, a)) def test_combination_changed_callback(self): @@ -191,7 +191,7 @@ def test_init_fails(self): Mapping(**cfg) # matching type, code and symbol - a = system_mapping.get("a") + a = keyboard_layout.get("a") cfg["output_code"] = a cfg["output_symbol"] = "a" cfg["output_type"] = EV_KEY diff --git a/tests/unit/test_system_mapping.py b/tests/unit/test_system_mapping.py index 3b2a5d462..a7aa2cee9 100644 --- a/tests/unit/test_system_mapping.py +++ b/tests/unit/test_system_mapping.py @@ -27,25 +27,25 @@ from evdev.ecodes import BTN_LEFT, KEY_A from inputremapper.configs.paths import PathUtils -from inputremapper.configs.system_mapping import SystemMapping, XMODMAP_FILENAME +from inputremapper.configs.keyboard_layout import KeyboardLayout, XMODMAP_FILENAME from tests.lib.test_setup import test_setup @test_setup class TestSystemMapping(unittest.TestCase): def test_update(self): - system_mapping = SystemMapping() - system_mapping.update({"foo1": 101, "bar1": 102}) - system_mapping.update({"foo2": 201, "bar2": 202}) - self.assertEqual(system_mapping.get("foo1"), 101) - self.assertEqual(system_mapping.get("bar2"), 202) + keyboard_layout = KeyboardLayout() + keyboard_layout.update({"foo1": 101, "bar1": 102}) + keyboard_layout.update({"foo2": 201, "bar2": 202}) + self.assertEqual(keyboard_layout.get("foo1"), 101) + self.assertEqual(keyboard_layout.get("bar2"), 202) def test_xmodmap_file(self): - system_mapping = SystemMapping() + keyboard_layout = KeyboardLayout() path = os.path.join(PathUtils.config_path(), XMODMAP_FILENAME) os.remove(path) - system_mapping.populate() + keyboard_layout.populate() self.assertTrue(os.path.exists(path)) with open(path, "r") as file: content = json.load(file) @@ -67,11 +67,11 @@ def check_output(*args, **kwargs): return SubprocessMock() with patch.object(subprocess, "check_output", check_output): - system_mapping = SystemMapping() + keyboard_layout = KeyboardLayout() path = os.path.join(PathUtils.config_path(), XMODMAP_FILENAME) os.remove(path) - system_mapping.populate() + keyboard_layout.populate() self.assertFalse(os.path.exists(path)) def test_xmodmap_command_missing(self): @@ -80,61 +80,61 @@ def check_output(*args, **kwargs): raise FileNotFoundError with patch.object(subprocess, "check_output", check_output): - system_mapping = SystemMapping() + keyboard_layout = KeyboardLayout() path = os.path.join(PathUtils.config_path(), XMODMAP_FILENAME) os.remove(path) - system_mapping.populate() + keyboard_layout.populate() self.assertFalse(os.path.exists(path)) def test_correct_case(self): - system_mapping = SystemMapping() - system_mapping.clear() - system_mapping._set("A", 31) - system_mapping._set("a", 32) - system_mapping._set("abcd_B", 33) - - self.assertEqual(system_mapping.correct_case("a"), "a") - self.assertEqual(system_mapping.correct_case("A"), "A") - self.assertEqual(system_mapping.correct_case("ABCD_b"), "abcd_B") + keyboard_layout = KeyboardLayout() + keyboard_layout.clear() + keyboard_layout._set("A", 31) + keyboard_layout._set("a", 32) + keyboard_layout._set("abcd_B", 33) + + self.assertEqual(keyboard_layout.correct_case("a"), "a") + self.assertEqual(keyboard_layout.correct_case("A"), "A") + self.assertEqual(keyboard_layout.correct_case("ABCD_b"), "abcd_B") # unknown stuff is returned as is - self.assertEqual(system_mapping.correct_case("FOo"), "FOo") + self.assertEqual(keyboard_layout.correct_case("FOo"), "FOo") - self.assertEqual(system_mapping.get("A"), 31) - self.assertEqual(system_mapping.get("a"), 32) - self.assertEqual(system_mapping.get("ABCD_b"), 33) - self.assertEqual(system_mapping.get("abcd_B"), 33) + self.assertEqual(keyboard_layout.get("A"), 31) + self.assertEqual(keyboard_layout.get("a"), 32) + self.assertEqual(keyboard_layout.get("ABCD_b"), 33) + self.assertEqual(keyboard_layout.get("abcd_B"), 33) - def test_system_mapping(self): - system_mapping = SystemMapping() - system_mapping.populate() - self.assertGreater(len(system_mapping._mapping), 100) + def test_keyboard_layout(self): + keyboard_layout = KeyboardLayout() + keyboard_layout.populate() + self.assertGreater(len(keyboard_layout._mapping), 100) # this is case-insensitive - self.assertEqual(system_mapping.get("1"), 2) - self.assertEqual(system_mapping.get("KeY_1"), 2) + self.assertEqual(keyboard_layout.get("1"), 2) + self.assertEqual(keyboard_layout.get("KeY_1"), 2) - self.assertEqual(system_mapping.get("AlT_L"), 56) - self.assertEqual(system_mapping.get("KEy_LEFtALT"), 56) + self.assertEqual(keyboard_layout.get("AlT_L"), 56) + self.assertEqual(keyboard_layout.get("KEy_LEFtALT"), 56) - self.assertEqual(system_mapping.get("kEY_LeFTSHIFT"), 42) - self.assertEqual(system_mapping.get("ShiFt_L"), 42) + self.assertEqual(keyboard_layout.get("kEY_LeFTSHIFT"), 42) + self.assertEqual(keyboard_layout.get("ShiFt_L"), 42) - self.assertEqual(system_mapping.get("BTN_left"), 272) + self.assertEqual(keyboard_layout.get("BTN_left"), 272) - self.assertIsNotNone(system_mapping.get("KEY_KP4")) - self.assertEqual(system_mapping.get("KP_Left"), system_mapping.get("KEY_KP4")) + self.assertIsNotNone(keyboard_layout.get("KEY_KP4")) + self.assertEqual(keyboard_layout.get("KP_Left"), keyboard_layout.get("KEY_KP4")) # this only lists the correct casing, # includes linux constants and xmodmap symbols - names = system_mapping.list_names() + names = keyboard_layout.list_names() self.assertIn("2", names) self.assertIn("c", names) self.assertIn("KEY_3", names) self.assertNotIn("key_3", names) self.assertIn("KP_Down", names) self.assertNotIn("kp_down", names) - names = system_mapping._mapping.keys() + names = keyboard_layout._mapping.keys() self.assertIn("F4", names) self.assertNotIn("f4", names) self.assertIn("BTN_RIGHT", names) @@ -143,22 +143,22 @@ def test_system_mapping(self): self.assertIn("KP_Home", names) self.assertNotIn("kp_home", names) - self.assertEqual(system_mapping.get("disable"), -1) + self.assertEqual(keyboard_layout.get("disable"), -1) def test_get_name_no_xmodmap(self): # if xmodmap is not installed, uses the linux constant names - system_mapping = SystemMapping() + keyboard_layout = KeyboardLayout() def check_output(*args, **kwargs): raise FileNotFoundError with patch.object(subprocess, "check_output", check_output): - system_mapping.populate() - self.assertEqual(system_mapping.get_name(KEY_A), "KEY_A") + keyboard_layout.populate() + self.assertEqual(keyboard_layout.get_name(KEY_A), "KEY_A") # testing for BTN_LEFT is especially important, because # `evdev.ecodes.BTN.get(code)` returns an array of ['BTN_LEFT', 'BTN_MOUSE'] - self.assertEqual(system_mapping.get_name(BTN_LEFT), "BTN_LEFT") + self.assertEqual(keyboard_layout.get_name(BTN_LEFT), "BTN_LEFT") if __name__ == "__main__": From 166792b0feff9da37a25e6b393185192e307b1c5 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:56:46 +0200 Subject: [PATCH 29/32] fix ci import_control maybe --- inputremapper/gui/utils.py | 1 + tests/unit/test_control.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/inputremapper/gui/utils.py b/inputremapper/gui/utils.py index 8da31547c..20b55c511 100644 --- a/inputremapper/gui/utils.py +++ b/inputremapper/gui/utils.py @@ -173,6 +173,7 @@ def wrapped(self, *args, **kwargs): return decorator + debounce_manager = DebounceManager() diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index 8e377aa4b..a7e4fe31a 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -27,6 +27,7 @@ from importlib.machinery import SourceFileLoader from importlib.util import spec_from_loader, module_from_spec from unittest.mock import patch +import re from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.migrations import Migrations @@ -43,8 +44,7 @@ def import_control(): """Import the core function of the input-remapper-control command.""" bin_path = os.path.join( - os.getcwd().rsplit("input-remapper")[0], - "input-remapper", + re.sub("/tests.*", "", os.getcwd()), "bin", "input-remapper-control", ) From 81f0dce4dbb9b013428c1858135a8b739833cc7e Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Thu, 3 Oct 2024 21:52:38 +0200 Subject: [PATCH 30/32] some static method refactoring for macros --- inputremapper/injection/macros/macro.py | 236 ++++++++++++------------ tests/unit/test_macros.py | 125 ++++++++----- 2 files changed, 196 insertions(+), 165 deletions(-) diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index 0876e7d7d..90eb4f57f 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -89,78 +89,6 @@ def __repr__(self): return f'' -def _type_check(value: Any, allowed_types, display_name=None, position=None) -> Any: - """Validate a parameter used in a macro. - - If the value is a Variable, it will be returned and should be resolved - during runtime with _resolve. - """ - if isinstance(value, Variable): - # it is a variable and will be read at runtime - return value - - for allowed_type in allowed_types: - if allowed_type is None: - if value is None: - return value - - continue - - # try to parse "1" as 1 if possible - if allowed_type != Macro: - # the macro constructor with a single argument always succeeds, - # but will definitely not result in the correct macro - try: - return allowed_type(value) - except (TypeError, ValueError): - pass - - if isinstance(value, allowed_type): - return value - - if display_name is not None and position is not None: - raise MacroParsingError( - msg=f"Expected parameter {position} for {display_name} to be " - f"one of {allowed_types}, but got {value}" - ) - - raise MacroParsingError( - msg=f"Expected parameter to be one of {allowed_types}, but got {value}" - ) - - -def _type_check_variablename(name: str): - """Check if this is a legit variable name. - - Because they could clash with language features. If the macro is able to be - parsed at all due to a problematic choice of a variable name. - - Allowed examples: "foo", "Foo1234_", "_foo_1234" - Not allowed: "1_foo", "foo=blub", "$foo", "foo,1234", "foo()" - """ - if not isinstance(name, str) or not re.match(r"^[A-Za-z_][A-Za-z_0-9]*$", name): - raise MacroParsingError(msg=f'"{name}" is not a legit variable name') - - -def _resolve(argument, allowed_types=None): - """If the argument is a variable, figure out its value and cast it. - - Variables are prefixed with `$` in the syntax. - - Use this just-in-time when you need the actual value of the variable - during runtime. - """ - if isinstance(argument, Variable): - value = argument.resolve() - logger.debug('"%s" is "%s"', argument, value) - if allowed_types: - return _type_check(value, allowed_types) - else: - return value - - return argument - - class Macro: """Supports chaining and preparing actions. @@ -312,10 +240,10 @@ def add_key(self, symbol: str): async def task(handler: Callable): # if the code is $foo, figure out the correct code now. - resolved_symbol = _resolve(symbol, [str]) + resolved_symbol = self._resolve(symbol, [str]) code = self._type_check_symbol(resolved_symbol) - resolved_code = _resolve(code, [int]) + resolved_code = self._resolve(code, [int]) handler(EV_KEY, resolved_code, 1) await self._keycode_pause() handler(EV_KEY, resolved_code, 0) @@ -328,10 +256,10 @@ def add_key_down(self, symbol: str): self._type_check_symbol(symbol) async def task(handler: Callable): - resolved_symbol = _resolve(symbol, [str]) + resolved_symbol = self._resolve(symbol, [str]) code = self._type_check_symbol(resolved_symbol) - resolved_code = _resolve(code, [int]) + resolved_code = self._resolve(code, [int]) handler(EV_KEY, resolved_code, 1) self.tasks.append(task) @@ -341,17 +269,17 @@ def add_key_up(self, symbol: str): self._type_check_symbol(symbol) async def task(handler: Callable): - resolved_symbol = _resolve(symbol, [str]) + resolved_symbol = self._resolve(symbol, [str]) code = self._type_check_symbol(resolved_symbol) - resolved_code = _resolve(code, [int]) + resolved_code = self._resolve(code, [int]) handler(EV_KEY, resolved_code, 0) self.tasks.append(task) def add_hold(self, macro=None): """Loops the execution until key release.""" - _type_check(macro, [Macro, str, None], "hold", 1) + self._type_check(macro, [Macro, str, None], "hold", 1) if macro is None: self.tasks.append(lambda _: self._trigger_release_event.wait()) @@ -364,10 +292,10 @@ def add_hold(self, macro=None): self._type_check_symbol(symbol) async def task(handler: Callable): - resolved_symbol = _resolve(symbol, [str]) + resolved_symbol = self._resolve(symbol, [str]) code = self._type_check_symbol(resolved_symbol) - resolved_code = _resolve(code, [int]) + resolved_code = self._resolve(code, [int]) handler(EV_KEY, resolved_code, 1) await self._trigger_release_event.wait() handler(EV_KEY, resolved_code, 0) @@ -395,14 +323,14 @@ def add_modify(self, modifier: str, macro: Macro): modifier macro """ - _type_check(macro, [Macro], "modify", 2) + self._type_check(macro, [Macro], "modify", 2) self._type_check_symbol(modifier) self.child_macros.append(macro) async def task(handler: Callable): # TODO test var - resolved_modifier = _resolve(modifier, [str]) + resolved_modifier = self._resolve(modifier, [str]) code = self._type_check_symbol(resolved_modifier) handler(EV_KEY, code, 1) @@ -419,7 +347,7 @@ def add_hold_keys(self, *symbols): self._type_check_symbol(symbol) async def task(handler: Callable): - resolved_symbols = [_resolve(symbol, [str]) for symbol in symbols] + resolved_symbols = [self._resolve(symbol, [str]) for symbol in symbols] codes = [self._type_check_symbol(symbol) for symbol in resolved_symbols] for code in codes: @@ -436,11 +364,11 @@ async def task(handler: Callable): def add_repeat(self, repeats: Union[str, int], macro: Macro): """Repeat actions.""" - repeats = _type_check(repeats, [int], "repeat", 1) - _type_check(macro, [Macro], "repeat", 2) + repeats = self._type_check(repeats, [int], "repeat", 1) + self._type_check(macro, [Macro], "repeat", 2) async def task(handler: Callable): - for _ in range(_resolve(repeats, [int])): + for _ in range(self._resolve(repeats, [int])): await macro.run(handler) self.tasks.append(task) @@ -457,9 +385,9 @@ def add_event(self, type_: Union[str, int], code: Union[str, int], value: int): examples: 52, 'KEY_A' value """ - type_ = _type_check(type_, [int, str], "event", 1) - code = _type_check(code, [int, str], "event", 2) - value = _type_check(value, [int, str], "event", 3) + type_ = self._type_check(type_, [int, str], "event", 1) + code = self._type_check(code, [int, str], "event", 2) + value = self._type_check(value, [int, str], "event", 3) if isinstance(type_, str): type_ = ecodes[type_.upper()] @@ -476,9 +404,9 @@ def add_mouse( acceleration: Optional[float] = None, ): """Move the mouse cursor.""" - _type_check(direction, [str], "mouse", 1) - speed = _type_check(speed, [int], "mouse", 2) - acceleration = _type_check(acceleration, [float, None], "mouse", 3) + self._type_check(direction, [str], "mouse", 1) + speed = self._type_check(speed, [int], "mouse", 2) + acceleration = self._type_check(acceleration, [float, None], "mouse", 3) code, value = { "up": (REL_Y, -1), @@ -488,8 +416,8 @@ def add_mouse( }[direction.lower()] async def task(handler: Callable): - resolved_speed = _resolve(speed, [int]) - resolved_accel = _resolve(acceleration, [float, None]) + resolved_speed = self._resolve(speed, [int]) + resolved_accel = self._resolve(acceleration, [float, None]) if resolved_accel: current_speed = 0.0 @@ -517,8 +445,8 @@ async def task(handler: Callable): def add_wheel(self, direction: str, speed: int): """Move the scroll wheel.""" - _type_check(direction, [str], "wheel", 1) - speed = _type_check(speed, [int], "wheel", 2) + self._type_check(direction, [str], "wheel", 1) + speed = self._type_check(speed, [int], "wheel", 2) code, value = { "up": ([REL_WHEEL, REL_WHEEL_HI_RES], [1 / 120, 1]), @@ -528,7 +456,7 @@ def add_wheel(self, direction: str, speed: int): }[direction.lower()] async def task(handler: Callable): - resolved_speed = _resolve(speed, [int]) + resolved_speed = self._resolve(speed, [int]) remainder = [0.0, 0.0] while self.is_holding(): for i in range(0, 2): @@ -542,20 +470,20 @@ async def task(handler: Callable): def add_wait(self, time: Union[int, float]): """Wait time in milliseconds.""" - time = _type_check(time, [int, float], "wait", 1) + time = self._type_check(time, [int, float], "wait", 1) async def task(_): - await asyncio.sleep(_resolve(time, [int, float]) / 1000) + await asyncio.sleep(self._resolve(time, [int, float]) / 1000) self.tasks.append(task) def add_set(self, variable: str, value): """Set a variable to a certain value.""" - _type_check_variablename(variable) + self._type_check_variablename(variable) async def task(_): # can also copy with set(a, $b) - resolved_value = _resolve(value) + resolved_value = self._resolve(value) logger.debug('"%s" set to "%s"', variable, resolved_value) macro_variables[variable] = value @@ -563,8 +491,8 @@ async def task(_): def add_add(self, variable: str, value: Union[int, float]): """Add a number to a variable.""" - _type_check_variablename(variable) - _type_check(value, [int, float], "value", 1) + self._type_check_variablename(variable) + self._type_check(value, [int, float], "value", 1) async def task(_): current = macro_variables[variable] @@ -573,7 +501,7 @@ async def task(_): macro_variables[variable] = 0 current = 0 - resolved_value = _resolve(value) + resolved_value = self._resolve(value) if not isinstance(resolved_value, (int, float)): logger.error('Expected delta "%s" to be a number', resolved_value) return @@ -598,8 +526,8 @@ def add_ifeq(self, variable, value, then=None, else_=None): "foo" without breaking old functionality, because "foo" is treated as a variable name. """ - _type_check(then, [Macro, None], "ifeq", 3) - _type_check(else_, [Macro, None], "ifeq", 4) + self._type_check(then, [Macro, None], "ifeq", 3) + self._type_check(else_, [Macro, None], "ifeq", 4) async def task(handler: Callable): set_value = macro_variables.get(variable) @@ -619,12 +547,12 @@ async def task(handler: Callable): def add_if_eq(self, value_1, value_2, then=None, else_=None): """Compare two values.""" - _type_check(then, [Macro, None], "if_eq", 3) - _type_check(else_, [Macro, None], "if_eq", 4) + self._type_check(then, [Macro, None], "if_eq", 3) + self._type_check(else_, [Macro, None], "if_eq", 4) async def task(handler: Callable): - resolved_value_1 = _resolve(value_1) - resolved_value_2 = _resolve(value_2) + resolved_value_1 = self._resolve(value_1) + resolved_value_2 = self._resolve(value_2) if resolved_value_1 == resolved_value_2: if then is not None: await then.run(handler) @@ -646,9 +574,9 @@ def add_if_tap(self, then=None, else_=None, timeout=300): macro key pressed -> released (does other stuff in the meantime) -> if_tap starts -> pressed -> released -> then """ - _type_check(then, [Macro, None], "if_tap", 1) - _type_check(else_, [Macro, None], "if_tap", 2) - timeout = _type_check(timeout, [int, float], "if_tap", 3) + self._type_check(then, [Macro, None], "if_tap", 1) + self._type_check(else_, [Macro, None], "if_tap", 2) + timeout = self._type_check(timeout, [int, float], "if_tap", 3) if isinstance(then, Macro): self.child_macros.append(then) @@ -664,7 +592,7 @@ async def wait(): await self._trigger_release_event.wait() async def task(handler: Callable): - resolved_timeout = _resolve(timeout, [int, float]) / 1000 + resolved_timeout = self._resolve(timeout, [int, float]) / 1000 try: await asyncio.wait_for(wait(), resolved_timeout) if then: @@ -677,8 +605,8 @@ async def task(handler: Callable): def add_if_single(self, then, else_, timeout=None): """If a key was pressed without combining it.""" - _type_check(then, [Macro, None], "if_single", 1) - _type_check(else_, [Macro, None], "if_single", 2) + self._type_check(then, [Macro, None], "if_single", 1) + self._type_check(else_, [Macro, None], "if_single", 2) if isinstance(then, Macro): self.child_macros.append(then) @@ -700,7 +628,7 @@ async def listener(event): self.context.listeners.add(listener) - resolved_timeout = _resolve(timeout, allowed_types=[int, float, None]) + resolved_timeout = Macro._resolve(timeout, allowed_types=[int, float, None]) await asyncio.wait( [ asyncio.Task(listener_done.wait()), @@ -741,3 +669,75 @@ def _type_check_symbol(self, keyname: Union[str, Variable]) -> Union[Variable, i raise SymbolNotAvailableInTargetError(symbol, target) return code + + @staticmethod + def _type_check(value: Any, allowed_types, display_name=None, position=None) -> Any: + """Validate a parameter used in a macro. + + If the value is a Variable, it will be returned and should be resolved + during runtime with _resolve. + """ + if isinstance(value, Variable): + # it is a variable and will be read at runtime + return value + + for allowed_type in allowed_types: + if allowed_type is None: + if value is None: + return value + + continue + + # try to parse "1" as 1 if possible + if allowed_type != Macro: + # the macro constructor with a single argument always succeeds, + # but will definitely not result in the correct macro + try: + return allowed_type(value) + except (TypeError, ValueError): + pass + + if isinstance(value, allowed_type): + return value + + if display_name is not None and position is not None: + raise MacroParsingError( + msg=f"Expected parameter {position} for {display_name} to be " + f"one of {allowed_types}, but got {value}" + ) + + raise MacroParsingError( + msg=f"Expected parameter to be one of {allowed_types}, but got {value}" + ) + + @staticmethod + def _type_check_variablename(name: str): + """Check if this is a legit variable name. + + Because they could clash with language features. If the macro is able to be + parsed at all due to a problematic choice of a variable name. + + Allowed examples: "foo", "Foo1234_", "_foo_1234" + Not allowed: "1_foo", "foo=blub", "$foo", "foo,1234", "foo()" + """ + if not isinstance(name, str) or not re.match(r"^[A-Za-z_][A-Za-z_0-9]*$", name): + raise MacroParsingError(msg=f'"{name}" is not a legit variable name') + + @staticmethod + def _resolve(argument, allowed_types=None): + """If the argument is a variable, figure out its value and cast it. + + Variables are prefixed with `$` in the syntax. + + Use this just-in-time when you need the actual value of the variable + during runtime. + """ + if isinstance(argument, Variable): + value = argument.resolve() + logger.debug('"%s" is "%s"', argument, value) + if allowed_types: + return Macro._type_check(value, allowed_types) + else: + return value + + return argument diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index 92c3f7915..6f0b5ce44 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -49,10 +49,7 @@ from inputremapper.injection.global_uinputs import GlobalUInputs, UInput from inputremapper.injection.macros.macro import ( Macro, - _type_check, macro_variables, - _type_check_variablename, - _resolve, Variable, ) from inputremapper.injection.macros.parse import ( @@ -75,6 +72,10 @@ class MacroTestBase(unittest.IsolatedAsyncioTestCase): + @classmethod + def setUpClass(cls): + macro_variables.start() + def setUp(self): self.result = [] self.global_uinputs = GlobalUInputs(UInput) @@ -219,80 +220,110 @@ async def test_count_brackets(self): self.assertEqual(_count_brackets("a(b(c))d()"), 7) def test_resolve(self): - self.assertEqual(_resolve("a"), "a") - self.assertEqual(_resolve(1), 1) - self.assertEqual(_resolve(None), None) + self.assertEqual(Macro._resolve("a"), "a") + self.assertEqual(Macro._resolve(1), 1) + self.assertEqual(Macro._resolve(None), None) # $ is part of a custom string here - self.assertEqual(_resolve('"$a"'), '"$a"') - self.assertEqual(_resolve("'$a'"), "'$a'") + self.assertEqual(Macro._resolve('"$a"'), '"$a"') + self.assertEqual(Macro._resolve("'$a'"), "'$a'") # variables are expected to be of the Variable type here, not a $string - self.assertEqual(_resolve("$a"), "$a") + self.assertEqual(Macro._resolve("$a"), "$a") variable = Variable("a") - self.assertEqual(_resolve(variable), None) + self.assertEqual(Macro._resolve(variable), None) macro_variables["a"] = 1 - self.assertEqual(_resolve(variable), 1) + self.assertEqual(Macro._resolve(variable), 1) def test_type_check(self): # allows params that can be cast to the target type - self.assertEqual(_type_check(1, [str, None], "foo", 0), "1") - self.assertEqual(_type_check("1", [int, None], "foo", 1), 1) - self.assertEqual(_type_check(1.2, [str], "foo", 2), "1.2") + self.assertEqual(Macro._type_check(1, [str, None], "foo", 0), "1") + self.assertEqual(Macro._type_check("1", [int, None], "foo", 1), 1) + self.assertEqual(Macro._type_check(1.2, [str], "foo", 2), "1.2") self.assertRaises( MacroParsingError, - lambda: _type_check("1.2", [int], "foo", 3), + lambda: Macro._type_check("1.2", [int], "foo", 3), + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check("a", [None], "foo", 0) + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check("a", [int], "foo", 1) ) - self.assertRaises(MacroParsingError, lambda: _type_check("a", [None], "foo", 0)) - self.assertRaises(MacroParsingError, lambda: _type_check("a", [int], "foo", 1)) self.assertRaises( MacroParsingError, - lambda: _type_check("a", [int, float], "foo", 2), + lambda: Macro._type_check("a", [int, float], "foo", 2), ) self.assertRaises( MacroParsingError, - lambda: _type_check("a", [int, None], "foo", 3), + lambda: Macro._type_check("a", [int, None], "foo", 3), ) - self.assertEqual(_type_check("a", [int, float, None, str], "foo", 4), "a") + self.assertEqual(Macro._type_check("a", [int, float, None, str], "foo", 4), "a") # variables are expected to be of the Variable type here, not a $string - self.assertRaises(MacroParsingError, lambda: _type_check("$a", [int], "foo", 4)) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check("$a", [int], "foo", 4) + ) variable = Variable("a") - self.assertEqual(_type_check(variable, [int], "foo", 4), variable) + self.assertEqual(Macro._type_check(variable, [int], "foo", 4), variable) self.assertRaises( MacroParsingError, - lambda: _type_check("a", [Macro], "foo", 0), + lambda: Macro._type_check("a", [Macro], "foo", 0), + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check(1, [Macro], "foo", 0) ) - self.assertRaises(MacroParsingError, lambda: _type_check(1, [Macro], "foo", 0)) - self.assertEqual(_type_check("1", [Macro, int], "foo", 4), 1) + self.assertEqual(Macro._type_check("1", [Macro, int], "foo", 4), 1) def test_type_check_variablename(self): - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("1a")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("$a")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("a()")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("1")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("+")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("-")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("*")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("a,b")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("a,b")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("#")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename(1)) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename(None)) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename([])) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename(())) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("1a") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("$a") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("a()") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("1") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("+") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("-") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("*") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("a,b") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("a,b") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("#") + ) + self.assertRaises(MacroParsingError, lambda: Macro._type_check_variablename(1)) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename(None) + ) + self.assertRaises(MacroParsingError, lambda: Macro._type_check_variablename([])) + self.assertRaises(MacroParsingError, lambda: Macro._type_check_variablename(())) # doesn't raise - _type_check_variablename("a") - _type_check_variablename("_a") - _type_check_variablename("_A") - _type_check_variablename("A") - _type_check_variablename("Abcd") - _type_check_variablename("Abcd_") - _type_check_variablename("Abcd_1234") - _type_check_variablename("Abcd1234_") + Macro._type_check_variablename("a") + Macro._type_check_variablename("_a") + Macro._type_check_variablename("_A") + Macro._type_check_variablename("A") + Macro._type_check_variablename("Abcd") + Macro._type_check_variablename("Abcd_") + Macro._type_check_variablename("Abcd_1234") + Macro._type_check_variablename("Abcd1234_") def test_split_keyword_arg(self): self.assertTupleEqual(_split_keyword_arg("_A=b"), ("_A", "b")) From 055047ac442a2d36ed3e671b1ebaaa58fcef3e3b Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:40:44 +0200 Subject: [PATCH 31/32] duplicate test_setup --- tests/integration/test_daemon.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_daemon.py b/tests/integration/test_daemon.py index cf8e4e669..1ed96839f 100644 --- a/tests/integration/test_daemon.py +++ b/tests/integration/test_daemon.py @@ -40,7 +40,6 @@ def gtk_iteration(): Gtk.main_iteration() -@test_setup @test_setup class TestDBusDaemon(unittest.TestCase): def setUp(self): From 73251d1f68e5691dce300d2c9ab92f045c388f96 Mon Sep 17 00:00:00 2001 From: sezanzeb <28510156+sezanzeb@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:01:58 +0200 Subject: [PATCH 32/32] mypy --- inputremapper/daemon.py | 2 +- tests/unit/test_reader.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index e63095347..eb2d0d6b8 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -345,7 +345,7 @@ def set_config_dir(self, config_dir: str): return self.config_dir = config_dir - self.global_config.load_config(config_path) + self.global_config.load_config(str(config_path)) def _autoload(self, group_key: str): """Check if autoloading is a good idea, and if so do it. diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index 6cb72d362..690753b54 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -111,6 +111,7 @@ async def create_reader_service(self, groups: Optional[_Groups] = None): groups = self.groups global_uinputs = GlobalUInputs(UInput) + assert groups is not None self.reader_service = ReaderService(groups, global_uinputs) asyncio.ensure_future(self.reader_service.run())