From cf01699c5fba4d24bb81cce5c2f8bf024ffee265 Mon Sep 17 00:00:00 2001
From: Stephen Kitt
Date: Sat, 5 Oct 2024 10:47:01 +0200
Subject: [PATCH 1/3] Use packaging.version instead of pkg_resources (#966)
pkg_resources is deprecated, this removes one set of uses.
Signed-off-by: Stephen Kitt
---
inputremapper/configs/mapping.py | 6 ++----
inputremapper/configs/migrations.py | 22 +++++++++++-----------
tests/unit/test_migrations.py | 6 +++---
3 files changed, 16 insertions(+), 18 deletions(-)
diff --git a/inputremapper/configs/mapping.py b/inputremapper/configs/mapping.py
index f98c320d3..dbe160c1f 100644
--- a/inputremapper/configs/mapping.py
+++ b/inputremapper/configs/mapping.py
@@ -21,9 +21,9 @@
import enum
from collections import namedtuple
+from packaging import version
from typing import Optional, Callable, Tuple, TypeVar, Union, Any, Dict
-import pkg_resources
from evdev.ecodes import (
EV_KEY,
EV_ABS,
@@ -84,9 +84,7 @@
# TODO: remove pydantic VERSION check as soon as we no longer support
# Ubuntu 20.04 and with it the ancient pydantic 1.2
-needs_workaround = pkg_resources.parse_version(
- str(VERSION)
-) < pkg_resources.parse_version("1.7.1")
+needs_workaround = version.parse(str(VERSION)) < version.parse("1.7.1")
EMPTY_MAPPING_NAME: str = _("Empty Mapping")
diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py
index 9b65cc73d..7c258e711 100644
--- a/inputremapper/configs/migrations.py
+++ b/inputremapper/configs/migrations.py
@@ -30,9 +30,9 @@
import re
import shutil
from pathlib import Path
+from packaging import version
from typing import Iterator, Tuple, Dict, List
-import pkg_resources
from evdev.ecodes import (
EV_KEY,
EV_ABS,
@@ -86,15 +86,15 @@ def config_version():
config_path = os.path.join(CONFIG_PATH, "config.json")
if not os.path.exists(config_path):
- return pkg_resources.parse_version("0.0.0")
+ return version.parse("0.0.0")
with open(config_path, "r") as file:
config = json.load(file)
if "version" in config.keys():
- return pkg_resources.parse_version(config["version"])
+ return version.parse(config["version"])
- return pkg_resources.parse_version("0.0.0")
+ return version.parse("0.0.0")
def _config_suffix():
@@ -481,27 +481,27 @@ def migrate():
v = config_version()
- if v < pkg_resources.parse_version("0.4.0"):
+ if v < version.parse("0.4.0"):
_config_suffix()
_preset_path()
- if v < pkg_resources.parse_version("1.2.2"):
+ if v < version.parse("1.2.2"):
_mapping_keys()
- if v < pkg_resources.parse_version("1.4.0"):
+ if v < version.parse("1.4.0"):
global_uinputs.prepare_all()
_add_target()
- if v < pkg_resources.parse_version("1.4.1"):
+ if v < version.parse("1.4.1"):
_otherwise_to_else()
- if v < pkg_resources.parse_version("1.5.0"):
+ if v < version.parse("1.5.0"):
_remove_logs()
- if v < pkg_resources.parse_version("1.6.0-beta"):
+ if v < version.parse("1.6.0-beta"):
_convert_to_individual_mappings()
# add new migrations here
- if v < pkg_resources.parse_version(VERSION):
+ if v < version.parse(VERSION):
_update_version()
diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py
index fa56abbb4..da6946bcf 100644
--- a/tests/unit/test_migrations.py
+++ b/tests/unit/test_migrations.py
@@ -20,7 +20,7 @@
import unittest
import shutil
import json
-import pkg_resources
+from packaging import version
from evdev.ecodes import (
EV_KEY,
@@ -380,7 +380,7 @@ def test_add_version(self):
file.write("{}")
migrate()
- self.assertEqual(pkg_resources.parse_version(VERSION), config_version())
+ self.assertEqual(version.parse(VERSION), config_version())
def test_update_version(self):
path = os.path.join(CONFIG_PATH, "config.json")
@@ -389,7 +389,7 @@ def test_update_version(self):
json.dump({"version": "0.1.0"}, file)
migrate()
- self.assertEqual(pkg_resources.parse_version(VERSION), config_version())
+ self.assertEqual(version.parse(VERSION), config_version())
def test_config_version(self):
path = os.path.join(CONFIG_PATH, "config.json")
From 4da1f7f20e01d8c7cfa67cdc3ad6a068079d78bb Mon Sep 17 00:00:00 2001
From: KAGEYAM4 <75798544+KAGEYAM4@users.noreply.github.com>
Date: Mon, 14 Oct 2024 21:02:00 +0530
Subject: [PATCH 2/3] Docs: clears misunderstanding on where Macros can be used
(#973)
* Docs: clears misunderstanding on where Macros can be used
* Apply suggestions from code review
Co-authored-by: Tobi <28510156+sezanzeb@users.noreply.github.com>
---------
Co-authored-by: Tobi <28510156+sezanzeb@users.noreply.github.com>
---
readme/usage.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/readme/usage.md b/readme/usage.md
index 422bb78bf..792e44517 100644
--- a/readme/usage.md
+++ b/readme/usage.md
@@ -13,10 +13,10 @@ as shown in the screenshots below.
-In the text input field, type the key to which you would like to map this input.
+In the "Output" textbox on the right, type the key to which you would like to map this input.
More information about the possible mappings can be found in
[examples.md](./examples.md) and [below](#key-names). You can also write your macro
-into the text input field. If you hit enter, it will switch to a multiline-editor with
+into the "Output" textbox. If you hit enter, it will switch to a multiline-editor with
line-numbers.
Changes are saved automatically. Press the "Apply" button to activate (inject) the
From 08226a24f940a4a0c62d61bb4f984c7fc593a08d Mon Sep 17 00:00:00 2001
From: Tobi <28510156+sezanzeb@users.noreply.github.com>
Date: Wed, 16 Oct 2024 21:15:11 +0200
Subject: [PATCH 3/3] Improved test setup (#960)
---
.github/workflows/test.yml | 4 +-
.run/All Tests.run.xml | 17 -
.run/Only Integration Tests.run.xml | 17 -
.run/Only Unit Tests.run.xml | 17 -
bin/input-remapper-control | 465 +++++-----
bin/input-remapper-gtk | 43 +-
bin/input-remapper-reader-service | 21 +-
bin/input-remapper-service | 26 +-
inputremapper/configs/base_config.py | 4 +-
inputremapper/configs/data.py | 4 +-
inputremapper/configs/global_config.py | 14 +-
inputremapper/configs/input_config.py | 4 +-
inputremapper/configs/mapping.py | 9 +-
inputremapper/configs/migrations.py | 807 +++++++++---------
inputremapper/configs/paths.py | 248 +++---
inputremapper/configs/preset.py | 8 +-
inputremapper/configs/system_mapping.py | 11 +-
inputremapper/configs/validation_errors.py | 6 +-
inputremapper/daemon.py | 21 +-
inputremapper/exceptions.py | 2 +-
inputremapper/groups.py | 14 +-
inputremapper/gui/autocompletion.py | 6 +-
inputremapper/gui/components/common.py | 2 +-
inputremapper/gui/components/device_groups.py | 4 +-
inputremapper/gui/components/editor.py | 2 +-
inputremapper/gui/components/main.py | 3 +-
inputremapper/gui/components/presets.py | 4 +-
inputremapper/gui/controller.py | 12 +-
inputremapper/gui/data_manager.py | 49 +-
inputremapper/gui/gettext.py | 2 +-
inputremapper/gui/messages/message_broker.py | 4 +-
inputremapper/gui/messages/message_data.py | 2 +-
inputremapper/gui/messages/message_types.py | 2 +-
inputremapper/gui/reader_client.py | 24 +-
inputremapper/gui/reader_service.py | 10 +-
inputremapper/gui/user_interface.py | 16 +-
inputremapper/gui/utils.py | 7 +-
inputremapper/injection/context.py | 6 +-
inputremapper/injection/event_reader.py | 6 +-
inputremapper/injection/global_uinputs.py | 47 +-
inputremapper/injection/injector.py | 4 +-
inputremapper/injection/macros/macro.py | 10 +-
inputremapper/injection/macros/parse.py | 4 +-
.../injection/mapping_handlers/__init__.py | 2 +-
.../mapping_handlers/abs_to_abs_handler.py | 6 +-
.../mapping_handlers/abs_to_btn_handler.py | 3 +-
.../mapping_handlers/abs_to_rel_handler.py | 4 +-
.../mapping_handlers/axis_switch_handler.py | 8 +-
.../mapping_handlers/axis_transform.py | 3 +-
.../mapping_handlers/combination_handler.py | 5 +-
.../mapping_handlers/hierarchy_handler.py | 3 +-
.../injection/mapping_handlers/key_handler.py | 6 +-
.../mapping_handlers/macro_handler.py | 4 +-
.../mapping_handlers/mapping_handler.py | 4 +-
.../mapping_handlers/mapping_parser.py | 4 +-
.../mapping_handlers/null_handler.py | 2 +-
.../mapping_handlers/rel_to_abs_handler.py | 6 +-
.../mapping_handlers/rel_to_btn_handler.py | 4 +-
.../mapping_handlers/rel_to_rel_handler.py | 6 +-
inputremapper/injection/numlock.py | 4 +-
inputremapper/input_event.py | 40 +-
inputremapper/ipc/__init__.py | 2 +-
inputremapper/ipc/pipe.py | 12 +-
inputremapper/ipc/shared_dict.py | 4 +-
inputremapper/ipc/socket.py | 10 +-
inputremapper/logger.py | 336 --------
inputremapper/logging/__init__.py | 0
inputremapper/logging/formatter.py | 143 ++++
inputremapper/logging/logger.py | 183 ++++
inputremapper/user.py | 64 +-
inputremapper/utils.py | 2 +-
readme/development.md | 18 +-
readme/usage.md | 10 +-
setup.py | 2 +-
tests/__init__.py | 2 -
tests/integration/__init__.py | 1 -
tests/integration/test_components.py | 39 +-
tests/integration/test_daemon.py | 13 +-
tests/integration/test_data.py | 35 +-
tests/integration/test_gui.py | 235 ++---
tests/integration/test_numlock.py | 13 +-
tests/integration/test_user_interface.py | 4 +-
tests/lib/cleanup.py | 13 +-
tests/lib/constants.py | 2 +-
tests/lib/fixture_pipes.py | 36 +
tests/lib/fixtures.py | 12 +-
tests/lib/global_uinputs.py | 2 +-
tests/lib/is_service_running.py | 32 +
tests/lib/logger.py | 6 +-
tests/lib/patches.py | 51 +-
tests/lib/pipes.py | 10 +-
tests/lib/project_root.py | 37 +
tests/lib/{stuff.py => spy.py} | 7 +-
tests/lib/test_setup.py | 111 +++
tests/lib/tmp.py | 2 +-
tests/lib/xmodmap.py | 2 +-
tests/test.py | 167 ----
tests/unit/__init__.py | 2 -
tests/unit/test_config.py | 17 +-
tests/unit/test_context.py | 21 +-
tests/unit/test_control.py | 159 ++--
tests/unit/test_controller.py | 219 +++--
tests/unit/test_daemon.py | 66 +-
tests/unit/test_data_manager.py | 102 +--
.../test_axis_transformation.py | 7 +-
.../test_event_pipeline.py | 11 +-
.../test_mapping_handlers.py | 15 +-
tests/unit/test_event_reader.py | 10 +-
tests/unit/test_global_uinputs.py | 17 +-
tests/unit/test_groups.py | 23 +-
tests/unit/test_injector.py | 59 +-
tests/unit/test_input_config.py | 5 +-
tests/unit/test_input_event.py | 4 +-
tests/unit/test_ipc.py | 19 +-
tests/unit/test_logger.py | 43 +-
tests/unit/test_macros.py | 10 +-
tests/unit/test_mapping.py | 5 +-
tests/unit/test_message_broker.py | 5 +-
tests/unit/test_migrations.py | 233 ++---
tests/unit/test_paths.py | 47 +-
tests/unit/test_preset.py | 52 +-
tests/unit/test_reader.py | 21 +-
tests/unit/test_system_mapping.py | 16 +-
tests/unit/test_test.py | 28 +-
tests/unit/test_user.py | 21 +-
tests/unit/test_util.py | 4 +-
126 files changed, 2667 insertions(+), 2294 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
delete mode 100644 inputremapper/logger.py
create mode 100644 inputremapper/logging/__init__.py
create mode 100644 inputremapper/logging/formatter.py
create mode 100644 inputremapper/logging/logger.py
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%)
create mode 100644 tests/lib/test_setup.py
delete mode 100644 tests/test.py
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d43023e35..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 }}
@@ -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
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/bin/input-remapper-control b/bin/input-remapper-control
index 639ffdb3f..499e1262d 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.
#
@@ -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.logging.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,155 @@ 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 since imports shouldn't run any code, this is fixed by moving towards DI
+ 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 since imports shouldn't run any code, this is fixed by moving towards DI
+ 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 +290,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..40cc25630 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.
#
@@ -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.logging.logger import logger
def start_processes() -> DaemonProxy:
@@ -53,19 +54,24 @@ 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))
+ # 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
@@ -77,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()
@@ -89,7 +95,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..e564b0426 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.
#
@@ -21,26 +21,29 @@
"""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.logging.logger import logger
-
-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)
+ 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 6d588e4ae..7e3cc2d9e 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.
#
@@ -25,29 +25,35 @@
import sys
from argparse import ArgumentParser
-from inputremapper.logger import update_verbosity, log_info
+from inputremapper.logging.logger import logger
-
-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:])
- 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/configs/base_config.py b/inputremapper/configs/base_config.py
index 85a5de588..a6a4407e6 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.
#
@@ -22,7 +22,7 @@
import copy
from typing import Union, List, Optional, Callable, Any
-from inputremapper.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 7635d2510..a1eca35dc 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.
#
@@ -27,7 +27,7 @@
import pkg_resources
-from inputremapper.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 9513151c2..3c08a5cd0 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.
#
@@ -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.logging.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)
@@ -130,4 +131,5 @@ def _save_config(self):
file.write("\n")
+# TODO DI
global_config = GlobalConfig()
diff --git a/inputremapper/configs/input_config.py b/inputremapper/configs/input_config.py
index 1424962b0..5648859a7 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.
#
@@ -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.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/mapping.py b/inputremapper/configs/mapping.py
index dbe160c1f..819e8238f 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.
#
@@ -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
@@ -391,7 +390,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 7c258e711..84d762959 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.
#
@@ -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
@@ -31,7 +31,7 @@
import shutil
from pathlib import Path
from packaging import version
-from typing import Iterator, Tuple, Dict, List
+from typing import Iterator, Tuple, Dict, List, Optional, TypedDict
from evdev.ecodes import (
EV_KEY,
@@ -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.logging.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 version.parse("0.0.0")
+ if v < version.parse("0.4.0"):
+ Migrations._config_suffix()
+ Migrations._preset_path()
- with open(config_path, "r") as file:
- config = json.load(file)
+ if v < version.parse("1.2.2"):
+ Migrations._mapping_keys()
- if "version" in config.keys():
- return version.parse(config["version"])
+ if v < version.parse("1.4.0"):
+ global_uinputs.prepare_all()
+ Migrations._add_target()
- return version.parse("0.0.0")
+ if v < version.parse("1.4.1"):
+ Migrations._otherwise_to_else()
+ if v < version.parse("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 < version.parse("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 < version.parse(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 version.parse("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 version.parse(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 version.parse("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() -> None:
+ """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 < version.parse("0.4.0"):
- _config_suffix()
- _preset_path()
-
- if v < version.parse("1.2.2"):
- _mapping_keys()
-
- if v < version.parse("1.4.0"):
- global_uinputs.prepare_all()
- _add_target()
-
- if v < version.parse("1.4.1"):
- _otherwise_to_else()
-
- if v < version.parse("1.5.0"):
- _remove_logs()
-
- if v < version.parse("1.6.0-beta"):
- _convert_to_individual_mappings()
-
- # add new migrations here
-
- if v < version.parse(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..c450619c4 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.
#
@@ -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.logging.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..973d11f39 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.
#
@@ -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.logging.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..f88493b3a 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.
#
@@ -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.logging.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)
@@ -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/inputremapper/configs/validation_errors.py b/inputremapper/configs/validation_errors.py
index 238256c05..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.
#
@@ -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..cf949e9cd 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.
#
@@ -38,13 +38,14 @@
gi.require_version("GLib", "2.0")
from gi.repository import GLib
-from inputremapper.logger import logger, is_debug
+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.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/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 b7846d5ef..ae9a86b0b 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.
#
@@ -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.logging.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..455be58cb 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.
#
@@ -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
@@ -39,7 +39,7 @@
get_macro_argument_names,
remove_comments,
)
-from inputremapper.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/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 d1ef1d8f3..01c994c76 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.
#
@@ -36,7 +36,7 @@
GroupData,
DoStackSwitch,
)
-from inputremapper.logger import logger
+from inputremapper.logging.logger import logger
class DeviceGroupEntry(FlowBoxEntry):
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 ec3544580..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.
#
@@ -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/components/presets.py b/inputremapper/gui/components/presets.py
index d552194f1..9adc1c08a 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.
#
@@ -37,7 +37,7 @@
PresetData,
DoStackSwitch,
)
-from inputremapper.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 fd854be8b..5b8e9fb87 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.
#
@@ -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,
@@ -47,8 +46,7 @@
MissingMacroOrKeyError,
OutputSymbolVariantError,
)
-from inputremapper.configs.paths import sanitize_path_component
-from inputremapper.configs.input_config import InputCombination, InputConfig
+from inputremapper.configs.paths import PathUtils
from inputremapper.configs.validation_errors import pydantify
from inputremapper.exceptions import DataManagementError
from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME
@@ -69,7 +67,7 @@
InjectorState,
InjectorStateMessage,
)
-from inputremapper.logger import logger
+from inputremapper.logging.logger import logger
if TYPE_CHECKING:
# avoids gtk import error in tests
@@ -494,7 +492,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..f4d89feae 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.
#
@@ -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 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
-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,
)
@@ -51,7 +50,7 @@
InjectorState,
InjectorStateMessage,
)
-from inputremapper.logger import logger
+from inputremapper.logging.logger import logger
DEFAULT_PRESET_NAME = _("new preset")
@@ -185,8 +184,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 +231,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 +249,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 +268,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 +279,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 +315,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 +364,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 +386,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 +401,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 +424,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/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 88b425c6d..13ed7e57b 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.
#
@@ -33,7 +33,7 @@
)
from inputremapper.gui.messages.message_types import MessageType
-from inputremapper.logger import logger
+from inputremapper.logging.logger import logger
if TYPE_CHECKING:
pass
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 65a31cf7a..91dd99429 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.
#
@@ -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,18 +48,10 @@
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 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 f5bb70e75..b0180e38d 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
@@ -65,8 +64,9 @@
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.logging.logger import logger
+from inputremapper.user import UserUtils
+from inputremapper.utils import get_device_hash
# 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..46b63be4a 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.
#
@@ -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,
@@ -63,8 +62,7 @@
gtk_iteration,
)
from inputremapper.injection.injector import InjectorStateMessage
-from inputremapper.logger import logger, COMMIT_HASH, VERSION, EVDEV_VERSION
-from inputremapper.gui.gettext import _
+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 58ec81e02..15122514d 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.
#
@@ -23,12 +23,9 @@
from dataclasses import dataclass
from typing import List, Callable, Dict, Optional
-import gi
-
from gi.repository import Gtk, GLib, Gdk
-from inputremapper.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 b247e1026..cf56608b1 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.
#
@@ -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,7 +37,8 @@
parse_mappings,
EventPipelines,
)
-from inputremapper.logger import logger
+from inputremapper.input_event import InputEvent
+from inputremapper.logging.logger import logger
class Context:
diff --git a/inputremapper/injection/event_reader.py b/inputremapper/injection/event_reader.py
index 67e16bc03..218e4049b 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.
#
@@ -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 import logger
+from inputremapper.logging.logger import logger
+from inputremapper.utils import get_device_hash, DeviceHash
class Context(Protocol):
diff --git a/inputremapper/injection/global_uinputs.py b/inputremapper/injection/global_uinputs.py
index 78b33e7fb..3fba120c7 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.
#
@@ -23,7 +23,7 @@
import inputremapper.exceptions
import inputremapper.utils
-from inputremapper.logger import logger
+from inputremapper.logging.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..35db52f49 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.
#
@@ -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.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 88e5c4a39..b0d3fd0e9 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.
#
@@ -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.logging.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..13af186cb 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.
#
@@ -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.logging.logger import logger
def is_this_a_macro(output: Any):
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 4756ac0ba..2159896fe 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.
#
@@ -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
@@ -33,7 +33,7 @@
InputEventHandler,
)
from inputremapper.input_event import InputEvent, EventActions
-from inputremapper.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_btn_handler.py b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py
index 136ed4baf..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.
#
@@ -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/abs_to_rel_handler.py b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py
index e2f0873ae..fe323b1a0 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.
#
@@ -49,7 +49,7 @@
InputEventHandler,
)
from inputremapper.input_event import InputEvent, EventActions
-from inputremapper.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 992e3b64f..f96b1fc0e 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.
#
@@ -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,
@@ -31,7 +31,7 @@
ContextProtocol,
)
from inputremapper.input_event import InputEvent, EventActions
-from inputremapper.logger import logger
+from inputremapper.logging.logger import logger
from inputremapper.utils import get_device_hash
diff --git a/inputremapper/injection/mapping_handlers/axis_transform.py b/inputremapper/injection/mapping_handlers/axis_transform.py
index 0460fc36f..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.
#
@@ -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 3b9b68c5d..0ec784867 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.
#
@@ -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
@@ -31,7 +32,7 @@
HandlerEnums,
)
from inputremapper.input_event import InputEvent
-from inputremapper.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/hierarchy_handler.py b/inputremapper/injection/mapping_handlers/hierarchy_handler.py
index 13d09b720..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.
#
@@ -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 5ab0f1f6a..7ce023fb4 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.
#
@@ -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
@@ -29,7 +29,7 @@
HandlerEnums,
)
from inputremapper.input_event import InputEvent
-from inputremapper.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 4f2d6e394..ed6af7103 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.
#
@@ -31,7 +31,7 @@
HandlerEnums,
)
from inputremapper.input_event import InputEvent
-from inputremapper.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 e610f7c00..87608a043 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.
#
@@ -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.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 3bc3fa1e7..65012e833 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.
#
@@ -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.logging.logger import logger
from inputremapper.utils import get_evdev_constant_name
EventPipelines = Dict[InputConfig, Set[InputEventHandler]]
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 5d670ec2f..e8b5ec6a4 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.
#
@@ -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,
@@ -47,7 +47,7 @@
InputEventHandler,
)
from inputremapper.input_event import InputEvent, EventActions
-from inputremapper.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 1f9bf1453..bbc39d6c8 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.
#
@@ -30,7 +30,7 @@
InputEventHandler,
)
from inputremapper.input_event import InputEvent, EventActions
-from inputremapper.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 98c5d6be0..d41a2c465 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.
#
@@ -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,
@@ -45,7 +45,7 @@
InputEventHandler,
)
from inputremapper.input_event import InputEvent
-from inputremapper.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 4006cb280..2598e24c5 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.
#
@@ -28,7 +28,7 @@
import re
import subprocess
-from inputremapper.logger import logger
+from inputremapper.logging.logger import logger
def is_numlock_on():
diff --git a/inputremapper/input_event.py b/inputremapper/input_event.py
index 6d3e7ad5d..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.
#
@@ -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/__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 74509bd5d..755bbac22 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.
#
@@ -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.logging.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..e2ab4bd84 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.
#
@@ -26,7 +26,7 @@
import select
from typing import Optional, Any
-from inputremapper.logger import logger
+from inputremapper.logging.logger import logger
class SharedDict:
diff --git a/inputremapper/ipc/socket.py b/inputremapper/ipc/socket.py
index 3a0510cd8..1b7613963 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.
#
@@ -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.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
@@ -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 16862ae73..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, stacklevel=2)
-
-
-# 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/logging/__init__.py b/inputremapper/logging/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/inputremapper/logging/formatter.py b/inputremapper/logging/formatter.py
new file mode 100644
index 000000000..ac08f63b8
--- /dev/null
+++ b/inputremapper/logging/formatter.py
@@ -0,0 +1,143 @@
+# -*- 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 .
+
+"""Logging setup for input-remapper."""
+
+import logging
+import os
+import sys
+from datetime import datetime
+from typing import Dict
+
+
+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: Dict[str, int] = {}
+
+ # 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) -> int:
+ return 16 + b + (6 * g) + (36 * r)
+
+ 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)
+ 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/logging/logger.py b/inputremapper/logging/logger.py
new file mode 100644
index 000000000..abc4f3992
--- /dev/null
+++ b/inputremapper/logging/logger.py
@@ -0,0 +1,183 @@
+# -*- 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 .
+
+"""Logging setup for input-remapper."""
+
+import logging
+import time
+from typing import cast
+
+from inputremapper.logging.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, stacklevel=2)
+
+ 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..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.
#
@@ -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/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/readme/development.md b/readme/development.md
index 6f32c0b74..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.
@@ -90,6 +83,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
---------
diff --git a/readme/usage.md b/readme/usage.md
index 792e44517..0ee50ed26 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
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/__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/__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 84869de38..6d1f23b70 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.
#
@@ -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")
@@ -34,8 +33,7 @@
gi.require_version("GtkSource", "4")
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
@@ -86,6 +84,7 @@
)
from inputremapper.configs.mapping import MappingData
from inputremapper.configs.input_config import InputCombination, InputConfig
+from tests.lib.test_setup import test_setup
class ComponentBaseTest(unittest.TestCase):
@@ -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.
@@ -169,6 +166,7 @@ def get_child_icons(flow_box: Gtk.FlowBox):
return icon_names
+@test_setup
class TestDeviceGroupSelection(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -233,6 +231,7 @@ def test_avoids_infinite_recursion(self):
self.controller_mock.load_group.assert_not_called()
+@test_setup
class TestTargetSelection(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -267,7 +266,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"))
@@ -280,6 +279,7 @@ def test_avoids_infinite_recursion(self):
self.controller_mock.update_mapping.assert_not_called()
+@test_setup
class TestPresetSelection(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -349,6 +349,7 @@ def test_loads_preset(self):
self.controller_mock.load_preset.assert_called_once_with("preset2")
+@test_setup
class TestMappingListbox(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -491,6 +492,7 @@ def test_sorts_empty_mapping_to_bottom(self):
self.assertEqual(bottom_row.combination, InputCombination.empty_combination())
+@test_setup
class TestMappingSelectionLabel(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -775,6 +777,7 @@ def test_removes_name_when_name_matches_combination(self):
self.controller_mock.update_mapping.assert_called_once_with(name="")
+@test_setup
class TestGdkEventRecorder(ComponentBaseTest):
def _emit_key(self, window, code, type_):
event = Gdk.Event()
@@ -825,6 +828,7 @@ def test_records_combinations(self):
self.assertEqual(label.get_text(), "a")
+@test_setup
class TestCodeEditor(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -928,6 +932,7 @@ def unfocus():
self.assertNotIn("opaque-text", self.gui.get_style_context().list_classes())
+@test_setup
class TestRecordingToggle(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -974,6 +979,7 @@ def test_shows_not_recording_when_recording_finished(self):
self.assert_not_recording()
+@test_setup
class TestStatusBar(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1057,6 +1063,7 @@ def test_sets_msg_as_tooltip_if_tooltip_is_none(self):
self.assertEqual(self.get_tooltip(), "msg")
+@test_setup
class TestAutoloadSwitch(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1084,6 +1091,7 @@ def test_avoids_infinite_recursion(self):
self.controller_mock.set_autoload.assert_not_called()
+@test_setup
class TestReleaseCombinationSwitch(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1115,6 +1123,7 @@ def test_avoids_infinite_recursion(self):
self.controller_mock.update_mapping.assert_not_called()
+@test_setup
class TestEventEntry(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1135,6 +1144,7 @@ def test_move_event(self):
)
+@test_setup
class TestCombinationListbox(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1197,6 +1207,7 @@ def test_avoids_infinite_recursion(self):
self.controller_mock.load_event.assert_not_called()
+@test_setup
class TestAnalogInputSwitch(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1240,6 +1251,7 @@ def test_enables_switch_when_axis_event(self):
self.assertTrue(self.gui.get_sensitive())
+@test_setup
class TestTriggerThresholdInput(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1285,6 +1297,7 @@ def test_updates_configuration_according_to_selected_event(self):
self.assert_key_event_config()
+@test_setup
class TestReleaseTimeoutInput(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1381,6 +1394,7 @@ def test_disables_input_based_on_input_combination(self):
self.assertLess(self.gui.get_opacity(), 0.6)
+@test_setup
class TestOutputAxisSelector(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1449,6 +1463,7 @@ def test_updates_dropdown_model(self):
self.assertEqual(len(self.gui.get_model()), 9)
+@test_setup
class TestKeyAxisStackSwitcher(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1511,6 +1526,7 @@ def test_avoids_infinite_recursion(self):
self.controller_mock.update_mapping.assert_not_called()
+@test_setup
class TestTransformationDrawArea(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1548,6 +1564,7 @@ def test_redraws_when_mapping_updates(self):
mock.assert_called()
+@test_setup
class TestSliders(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1624,6 +1641,7 @@ def test_avoids_recursion(self):
self.controller_mock.update_mapping.assert_not_called()
+@test_setup
class TestRelativeInputCutoffInput(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
@@ -1725,6 +1743,7 @@ def test_enables_input(self):
self.assert_active()
+@test_setup
class TestRequireActiveMapping(ComponentBaseTest):
def test_no_reqorded_input_required(self):
self.box = Gtk.Box()
@@ -1788,6 +1807,7 @@ def assert_active(self, widget: Gtk.Widget):
self.assertEqual(widget.get_opacity(), 1)
+@test_setup
class TestStack(ComponentBaseTest):
def test_switches_pages(self):
self.stack = Gtk.Stack()
@@ -1807,6 +1827,7 @@ def test_switches_pages(self):
self.assertEqual(self.stack.get_visible_child_name(), "Editor")
+@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 5edad3b58..1ed96839f 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.
#
@@ -18,20 +18,20 @@
# 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
-
-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
from inputremapper.daemon import Daemon, BUS_NAME
+from tests.lib.test_setup import test_setup
def gtk_iteration():
@@ -40,6 +40,7 @@ def gtk_iteration():
Gtk.main_iteration()
+@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 08d00d9db..9ed77a844 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.
#
@@ -18,32 +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]
-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 8224f58cb..be79a778f 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.
#
@@ -19,35 +19,20 @@
# along with input-remapper. If not, see .
import asyncio
-
-# the tests file needs to be imported first to make sure patches are loaded
-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,
@@ -56,13 +41,22 @@
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
-
-import gi
+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
+from tests.lib.spy import spy
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
@@ -72,7 +66,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 +88,7 @@
from inputremapper.injection.injector import InjectorState
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.daemon import Daemon, DaemonProxy
+from tests.lib.test_setup import test_setup
# iterate a few times when Gtk.main() is called, but don't block
@@ -144,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):
@@ -191,19 +189,10 @@ def get_keyval(self):
return True, self.keyval
+@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:
@@ -211,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,
@@ -224,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
@@ -250,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):
@@ -468,6 +481,7 @@ def sleep(self, num_events):
gtk_iteration()
+@test_setup
class TestColors(GuiTestBase):
# requires a running ui, otherwise fails with segmentation faults
def test_get_color_falls_back(self):
@@ -489,10 +503,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()
@@ -517,6 +531,7 @@ def test_get_colors(self):
self._test_color_wont_fallback(Colors.get_font_color, Colors.fallback_font)
+@test_setup
class TestGui(GuiTestBase):
"""For tests that use the window.
@@ -657,7 +672,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(), "")
@@ -1398,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()
@@ -1443,7 +1458,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())
@@ -1457,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))
@@ -1484,7 +1499,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 +1795,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 +1898,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):
+ with patch_confirm_delete(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):
+ with patch_confirm_delete(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")
@@ -1971,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")
@@ -1984,14 +2005,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):
@@ -2028,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()
@@ -2036,6 +2057,7 @@ def test_enable_disable_output(self):
self.assertFalse(self.output_box.get_sensitive())
+@test_setup
class TestAutocompletion(GuiTestBase):
def press_key(self, keyval):
event = Gdk.EventKey()
@@ -2249,6 +2271,7 @@ def test_cycling(self):
)
+@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 b3a96ef49..d4d7f706c 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.
#
@@ -19,21 +19,14 @@
# 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
+from tests.lib.test_setup import test_setup
+@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 87539bbf3..37365d124 100644
--- a/tests/integration/test_user_interface.py
+++ b/tests/integration/test_user_interface.py
@@ -10,14 +10,15 @@
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
from inputremapper.configs.mapping import MappingData
from inputremapper.configs.input_config import InputCombination, InputConfig
+from tests.lib.test_setup import test_setup
+@test_setup
class TestUserInterface(unittest.TestCase):
def setUp(self) -> None:
self.message_broker = MessageBroker()
@@ -30,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/cleanup.py b/tests/lib/cleanup.py
index 58af6458d..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
@@ -27,6 +28,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...
@@ -41,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())
@@ -73,13 +77,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 +130,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/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 13d5198da..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.
#
@@ -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/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 3af684489..ab7ace952 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.
#
@@ -37,9 +37,9 @@
def update_inputremapper_verbosity():
- from inputremapper.logger import update_verbosity
+ from inputremapper.logging.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..1d34b37da 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.
#
@@ -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:
@@ -201,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
@@ -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", 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),
+ ]
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,18 @@ 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 [
+ # 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(),
+ ]
diff --git a/tests/lib/pipes.py b/tests/lib/pipes.py
index bee33494e..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.
#
@@ -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/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/lib/test_setup.py b/tests/lib/test_setup.py
new file mode 100644
index 000000000..4961128e1
--- /dev/null
+++ b/tests/lib/test_setup.py
@@ -0,0 +1,111 @@
+#!/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
+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
+from tests.lib.logger import update_inputremapper_verbosity
+from tests.lib.patches import create_patches
+
+
+def test_setup(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()
+
+ # 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, 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()
+
+ # 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():
+ original_tearDownClass()
+
+ remove_fixture_pipes()
+
+ # 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):
+ for patch in patches:
+ patch.start()
+
+ original_setUp(self)
+
+ def tearDown(self):
+ original_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()
+
+ cls.setUp = setUp
+ cls.tearDown = tearDown
+ cls.setUpClass = setUpClass
+ cls.tearDownClass = tearDownClass
+
+ return cls
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/test.py b/tests/test.py
deleted file mode 100644
index 201078f2c..000000000
--- a/tests/test.py
+++ /dev/null
@@ -1,167 +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
-
-tracemalloc.start()
-
-# 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")
-
-
-# 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
-
-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.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
-
-
-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)
-
-
-# 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.
-patch_paths()
-patch_evdev()
-patch_events()
-patch_os_system()
-patch_check_output()
-patch_regrab_timeout()
-patch_is_running()
-# patch_warnings()
-
-
-update_inputremapper_verbosity()
-
-
-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/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..f80720eeb 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.
#
@@ -19,21 +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.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)
@@ -122,7 +119,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..87b6dacd6 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.
#
@@ -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,19 +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.input_config import InputCombination
from inputremapper.configs.mapping import Mapping
-from inputremapper.configs.input_config import InputConfig, InputCombination
+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 0a7fdba5a..f2d800385 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.
#
@@ -20,24 +20,21 @@
"""Testing the input-remapper-control command"""
-
-
-from tests.lib.cleanup import quick_cleanup
-from tests.lib.tmp import tmp
-
+import collections
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
+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.paths import PathUtils
from inputremapper.configs.preset import Preset
-from inputremapper.configs.paths import get_preset_path
+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():
@@ -53,10 +50,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,18 +62,19 @@ def import_control():
)
+@test_setup
class TestControl(unittest.TestCase):
- def tearDown(self):
- quick_cleanup()
+ def setUp(self):
+ self.input_remapper_control = InputRemapperControl()
def test_autoload(self):
device_keys = ["Foo Device 2", "Bar Device"]
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 +83,8 @@ def test_autoload(self):
daemon = Daemon()
+ self.input_remapper_control.set_daemon(daemon)
+
start_history = []
stop_counter = 0
@@ -99,12 +99,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 +123,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 +139,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 +152,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 +168,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 +182,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 +199,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 +237,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 +247,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 +263,7 @@ def test_start_stop(self):
preset = "preset9"
daemon = Daemon()
+ self.input_remapper_control.set_daemon(daemon)
start_history = []
stop_history = []
@@ -242,23 +272,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 +307,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 +356,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..39015835d 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
@@ -52,18 +51,19 @@
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 tests.lib.cleanup import quick_cleanup
-from tests.lib.stuff import spy
+from inputremapper.configs.mapping import UIMapping, MappingData, Mapping
+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
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.lib.test_setup import test_setup
+@test_setup
class TestController(unittest.TestCase):
def setUp(self) -> None:
super().setUp()
@@ -82,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(
@@ -231,7 +228,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 +250,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 +260,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 +307,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 +331,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 +378,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 +421,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 +598,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 +611,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 +619,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 +633,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..e6f01314a 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.
#
@@ -17,10 +17,12 @@
#
# 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.test import is_service_running
from tests.lib.logger import logger
from tests.lib.cleanup import cleanup
from tests.lib.fixtures import Fixture
@@ -31,7 +33,6 @@
import os
import unittest
import time
-import subprocess
import json
import evdev
@@ -42,26 +43,22 @@
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.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
- mkdir(get_config_path())
+ PathUtils.mkdir(PathUtils.get_config_path())
global_config._save_config()
# the daemon should be able to create them on demand:
@@ -73,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
@@ -99,21 +89,23 @@ 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
- 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 +209,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 +230,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 +240,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()
@@ -473,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]
@@ -482,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))
diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py
index 1583f794c..18c5873b3 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.
#
@@ -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
@@ -25,11 +26,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 +44,9 @@
)
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 get_preset_path
-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
class Listener:
@@ -57,6 +57,7 @@ def __call__(self, data):
self.calls.append(data)
+@test_setup
class TestDataManager(unittest.TestCase):
def setUp(self) -> None:
self.message_broker = MessageBroker()
@@ -72,9 +73,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()
@@ -135,7 +133,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 +165,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 +351,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 +371,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 +540,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 +561,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 +667,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 +688,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 +757,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 +798,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 +819,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 +835,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 +846,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 +862,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..08867df64 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.
#
@@ -17,15 +17,18 @@
#
# 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
+from tests.lib.test_setup import test_setup
+@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 078890cb1..97271993c 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.
#
@@ -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
@@ -63,6 +64,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.lib.test_setup import test_setup
class EventPipelineTestBase(unittest.IsolatedAsyncioTestCase):
@@ -108,6 +110,7 @@ def create_event_reader(
return reader
+@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
@@ -933,6 +936,7 @@ async def test_key_axis_combination_to_disable(self):
)
+@test_setup
class TestAbsToAbs(EventPipelineTestBase):
async def test_abs_to_abs(self):
gain = 0.5
@@ -1033,6 +1037,7 @@ async def test_abs_to_abs_with_input_switch(self):
)
+@test_setup
class TestRelToAbs(EventPipelineTestBase):
async def test_rel_to_abs(self):
timestamp = 0
@@ -1168,6 +1173,7 @@ async def test_rel_to_abs_with_input_switch(self):
)
+@test_setup
class TestAbsToRel(EventPipelineTestBase):
async def test_abs_to_rel(self):
"""Map gamepad EV_ABS events to EV_REL events."""
@@ -1314,6 +1320,7 @@ async def test_abs_to_wheel_hi_res_quirk(self):
self.assertAlmostEqual(rel_hwheel, rel_hwheel_hi_res / 120, places=0)
+@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."""
@@ -1446,6 +1453,7 @@ async def test_rel_trigger_threshold(self):
)
+@test_setup
class TestAbsToBtn(EventPipelineTestBase):
async def test_abs_trigger_threshold(self):
"""Test that different activation points for abs_to_btn work correctly."""
@@ -1514,6 +1522,7 @@ async def test_abs_trigger_threshold(self):
self.assertEqual(len(forwarded_history), 0)
+@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 05269889f..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.
#
@@ -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.lib.test_setup import test_setup
class BaseTests:
@@ -90,6 +90,7 @@ def test_reset(self):
mock.reset.assert_called()
+@test_setup
class TestAxisSwitchHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
@@ -110,6 +111,7 @@ def setUp(self):
)
+@test_setup
class TestAbsToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
@@ -125,6 +127,7 @@ def setUp(self):
)
+@test_setup
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):
)
+@test_setup
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)
+@test_setup
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)
+@test_setup
class TestCombinationHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
handler: CombinationHandler
@@ -382,6 +388,7 @@ def test_no_forwards(self):
self.assertListEqual(uinputs[self.keyboard_hash].write_history, [])
+@test_setup
class TestHierarchyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.mock1 = MagicMock()
@@ -399,6 +406,7 @@ def test_reset(self):
self.mock3.reset.assert_called()
+@test_setup
class TestKeyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
@@ -431,6 +439,7 @@ def test_reset(self):
self.assertEqual(len(history), 2)
+@test_setup
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)
+@test_setup
class TestRelToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
@@ -485,6 +495,7 @@ def setUp(self):
)
+@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 650ec6b39..c3e39a3ff 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.
#
@@ -36,19 +36,20 @@
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
+@test_setup
class TestEventReader(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.gamepad_source = evdev.InputDevice(fixtures.gamepad.path)
@@ -58,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_global_uinputs.py b/tests/unit/test_global_uinputs.py
index 199e483c5..458b8c1a7 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.
#
@@ -17,29 +17,29 @@
#
# 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 (
- EV_KEY,
- EV_ABS,
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
+@test_setup
class TestFrontendUinput(unittest.TestCase):
def setUp(self) -> None:
cleanup()
@@ -57,6 +57,7 @@ def test_init(self):
self.assertEqual(uinput_custom.capabilities(), capabilities)
+@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 2178bfbea..3f62bdc0c 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.
#
@@ -18,18 +18,14 @@
# 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.fixtures import fixtures, keyboard_keys
-
+import json
import os
import unittest
-import json
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 +33,8 @@
DeviceType,
_Group,
)
+from tests.lib.fixtures import fixtures, keyboard_keys
+from tests.lib.test_setup import test_setup
class FakePipe:
@@ -46,10 +44,8 @@ def send(self, groups):
self.groups = 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"],
@@ -61,7 +57,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..9636b375c 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.
#
@@ -23,22 +23,9 @@
except ImportError:
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 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 (
@@ -53,24 +40,32 @@
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
def wait_for_uinput_write():
@@ -80,6 +75,7 @@ def wait_for_uinput_write():
return float(time.time() - start)
+@test_setup
class TestInjector(unittest.IsolatedAsyncioTestCase):
new_gamepad_path = "/dev/input/event100"
@@ -87,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
@@ -111,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,11 +541,8 @@ def test_is_in_capabilities(self):
self.assertTrue(is_in_capabilities(key, capabilities))
+@test_setup
class TestModifyCapabilities(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- quick_cleanup()
-
def setUp(self):
class FakeDevice:
def __init__(self):
@@ -648,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_input_config.py b/tests/unit/test_input_config.py
index 2d4c205ce..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,8 +44,10 @@
)
from inputremapper.configs.input_config import InputCombination, InputConfig
+from tests.lib.test_setup import test_setup
+@test_setup
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"
+@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 aab44bd86..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,8 +23,10 @@
import evdev
from dataclasses import FrozenInstanceError
from inputremapper.input_event import InputEvent
+from tests.lib.test_setup import test_setup
+@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 e055e9775..70fc47188 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.
#
@@ -17,31 +17,28 @@
#
# You should have received a copy of the GNU General Public License
# 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
class TestSharedDict(unittest.TestCase):
def setUp(self):
self.shared_dict = SharedDict()
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"])
@@ -52,6 +49,7 @@ def test_set_get(self):
self.assertEqual(self.shared_dict["a"], 3)
+@test_setup
class TestSocket(unittest.TestCase):
def test_socket(self):
def test(s1, s2):
@@ -127,6 +125,7 @@ def test_base_abstract(self):
self.assertRaises(NotImplementedError, lambda: Base.fileno(None))
+@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 f80454715..c89e61bf2 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.
#
@@ -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.logging.logger import (
+ logger,
+ ColorfulFormatter,
+)
+from tests.lib.test_setup import test_setup
-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)
+@test_setup
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..d05b2b78d 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,
@@ -70,7 +69,7 @@
)
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
class MacroTestBase(unittest.IsolatedAsyncioTestCase):
@@ -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."""
@@ -123,6 +121,7 @@ class DummyMapping:
target_uinput = "keyboard + mouse"
+@test_setup
class TestMacros(MacroTestBase):
async def test_named_parameter(self):
result = []
@@ -1152,6 +1151,7 @@ async def test_multiline_macro_and_comments(self):
)
+@test_setup
class TestIfEq(MacroTestBase):
async def test_ifeq_runs(self):
# deprecated ifeq function, but kept for compatibility reasons
@@ -1304,6 +1304,7 @@ def set_foo(value):
)
+@test_setup
class TestIfSingle(MacroTestBase):
async def test_if_single(self):
macro = parse("if_single(key(x), key(y))", self.context, DummyMapping)
@@ -1451,6 +1452,7 @@ async def test_if_single_ignores_joystick(self):
self.assertListEqual(self.result, [(EV_KEY, code_a, 1), (EV_KEY, code_a, 0)])
+@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 2c5bfb227..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,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.lib.test_setup import test_setup
+@test_setup
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):
)
+@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 e5e45a4c1..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,6 +22,7 @@
from dataclasses import dataclass
from inputremapper.gui.messages.message_broker import MessageBroker, MessageType, Signal
+from tests.lib.test_setup import test_setup
class Listener:
@@ -38,6 +39,7 @@ class Message:
msg: str
+@test_setup
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:])
+@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 da6946bcf..08c494e74 100644
--- a/tests/unit/test_migrations.py
+++ b/tests/unit/test_migrations.py
@@ -12,14 +12,10 @@
# 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 unittest
from packaging import version
from evdev.ecodes import (
@@ -35,66 +31,55 @@
REL_Y,
REL_WHEEL_HI_RES,
REL_HWHEEL_HI_RES,
- KEY_A,
)
+from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import UIMapping
-from inputremapper.configs.migrations import migrate, config_version
+from inputremapper.configs.migrations import Migrations
+from inputremapper.configs.paths import PathUtils
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.logging.logger import VERSION
+from inputremapper.user import UserUtils
+from tests.lib.test_setup import test_setup
+from tests.lib.tmp import tmp
+@test_setup
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):
- quick_cleanup()
- 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 +90,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 +105,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 +135,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 +171,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 +200,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 +225,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 +314,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 +332,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 +379,41 @@ 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(version.parse(VERSION), config_version())
+ Migrations.migrate()
+ self.assertEqual(version.parse(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(version.parse(VERSION), config_version())
+ Migrations.migrate()
+ self.assertEqual(version.parse(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 +430,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 +497,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 +516,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 +584,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 +600,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 +623,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 +645,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 +670,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..c24c80e7d 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.
#
@@ -19,58 +19,55 @@
# 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 (
- touch,
- mkdir,
- get_preset_path,
- get_config_path,
- split_all,
-)
+from inputremapper.configs.paths import PathUtils
+from tests.lib.test_setup import test_setup
+from tests.lib.tmp import tmp
def _raise(error):
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")
- 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..ea4e96983 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.
#
@@ -24,22 +24,20 @@
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 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.lib.test_setup import test_setup
+@test_setup
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):
- 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))
@@ -62,7 +60,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 +80,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 +95,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 +128,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 +156,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 +219,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 +350,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 +382,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 +435,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 +456,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 +469,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..c0e0b5e46 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.
#
@@ -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,17 +53,17 @@
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.stuff import spy
+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
CODE_1 = 100
CODE_2 = 101
@@ -89,6 +89,7 @@ def wait(func, timeout=1.0):
break
+@test_setup
class TestReaderAsyncio(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.reader_service = None
@@ -97,7 +98,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):
@@ -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()
@@ -157,6 +158,7 @@ def remember_context(*args, **kwargs):
self.assertEqual([call[0] for call in write_spy.call_args_list], events)
+@test_setup
class TestReaderMultiprocessing(unittest.TestCase):
def setUp(self):
self.reader_service_process = None
@@ -165,7 +167,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 ab4010dbf..3b2a5d462 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.
#
@@ -26,15 +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.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})
@@ -44,7 +42,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 +68,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 +81,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..913eb1912 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.
#
@@ -17,38 +17,36 @@
#
# 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 asyncio
+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
+@test_setup
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 1322dbe97..d9e35f09c 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.
#
@@ -19,29 +19,26 @@
# along with input-remapper. If not, see .
-from tests.lib.cleanup import quick_cleanup
-
import os
import unittest
from unittest import mock
-from inputremapper.user import get_user, get_home
+from inputremapper.user import UserUtils
+from tests.lib.test_setup import test_setup
def _raise(error):
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(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 +47,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..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,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.lib.test_setup import test_setup
+@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