From 3c0d7d1c0a105206f214a24b89f283b1862a8079 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 17:00:59 +0000 Subject: [PATCH 01/72] build(deps-dev): bump isort from 5.11.3 to 5.11.4 Bumps [isort](https://github.com/pycqa/isort) from 5.11.3 to 5.11.4. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.11.3...5.11.4) --- updated-dependencies: - dependency-name: isort dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e0098f88..94b941b3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ flake8-annotations-coverage==0.0.6 flake8-cognitive-complexity==0.1.0 flake8-comprehensions==3.10.1 flake8-docstrings==1.6.0 -isort==5.11.3 +isort==5.11.4 flake8-isort==5.0.3 flake8-json==21.7.0 flake8-noqa==1.3.0 From 62b0a392bd2c65b15de8ba5c5d0ade15f9b01020 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 17:01:04 +0000 Subject: [PATCH 02/72] build(deps-dev): bump coverage-conditional-plugin from 0.7.0 to 0.8.0 Bumps [coverage-conditional-plugin](https://github.com/wemake-services/coverage-conditional-plugin) from 0.7.0 to 0.8.0. - [Release notes](https://github.com/wemake-services/coverage-conditional-plugin/releases) - [Changelog](https://github.com/wemake-services/coverage-conditional-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/wemake-services/coverage-conditional-plugin/compare/0.7.0...0.8.0) --- updated-dependencies: - dependency-name: coverage-conditional-plugin dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e0098f88..de633c6a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -46,7 +46,7 @@ py2exe==0.13.0.0; sys_platform == 'win32' pytest==7.2.0 pytest-cov==4.0.0 # Pytest code coverage support coverage[toml]==6.5.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs -coverage-conditional-plugin==0.7.0 +coverage-conditional-plugin==0.8.0 # For manipulating folder permissions and the like. pywin32==305; sys_platform == 'win32' From 70a9609ef0e56f149c0ea079c83618e1d4090a0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 17:01:12 +0000 Subject: [PATCH 03/72] build(deps-dev): bump types-requests from 2.28.11.6 to 2.28.11.7 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.11.6 to 2.28.11.7. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e0098f88..00bb8867 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -23,7 +23,7 @@ flake8-use-fstring==1.4 mypy==0.991 pep8-naming==0.13.3 safety==2.3.5 -types-requests==2.28.11.6 +types-requests==2.28.11.7 # Code formatting tools autopep8==2.0.1 From 07474304cfedfd5e70d8a3649da26710f7d7267c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Dec 2022 17:35:52 +0000 Subject: [PATCH 04/72] build(deps-dev): bump coverage[toml] from 6.5.0 to 7.0.0 Bumps [coverage[toml]](https://github.com/nedbat/coveragepy) from 6.5.0 to 7.0.0. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/6.5.0...7.0.0) --- updated-dependencies: - dependency-name: coverage[toml] dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 76958aaf..ceacac8e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,7 +45,7 @@ py2exe==0.13.0.0; sys_platform == 'win32' # Testing pytest==7.2.0 pytest-cov==4.0.0 # Pytest code coverage support -coverage[toml]==6.5.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs +coverage[toml]==7.0.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs coverage-conditional-plugin==0.8.0 # For manipulating folder permissions and the like. pywin32==305; sys_platform == 'win32' From 204ded5b105084b867b9872a7b37441e1cbc03d6 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 08:56:34 +0000 Subject: [PATCH 05/72] pre-commit: Add types-urllib3 to additional_dependencies It's in `pip freeze | grep types`, so might as well be explicit. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d841cda1..778c2161 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,7 +51,7 @@ repos: - id: mypy # verbose: true # log_file: 'pre-commit_mypy.log' - additional_dependencies: [ types-requests ] + additional_dependencies: [ types-requests, types-urllib3 ] # args: [ "--follow-imports", "skip", "--ignore-missing-imports", "--scripts-are-modules" ] ### # pydocstyle.exe From 48763890fe651b7c68f8f056e79fd51a7fd8eb78 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 08:57:31 +0000 Subject: [PATCH 06/72] config: Fix "could be None" types in __init__.py --- config/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index b3b240ed..ffc635e3 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -307,7 +307,10 @@ class AbstractConfig(abc.ABC): return None - def get(self, key: str, default: Union[list, str, bool, int] = None) -> Union[list, str, bool, int]: + def get( + self, key: str, + default: Union[list, str, bool, int, None] = None + ) -> Union[list, str, bool, int, None]: """ Return the data for the requested key, or a default. @@ -334,7 +337,7 @@ class AbstractConfig(abc.ABC): return default # type: ignore @abstractmethod - def get_list(self, key: str, *, default: list = None) -> list: + def get_list(self, key: str, *, default: list | None = None) -> list: """ Return the list referred to by the given key if it exists, or the default. @@ -343,7 +346,7 @@ class AbstractConfig(abc.ABC): raise NotImplementedError @abstractmethod - def get_str(self, key: str, *, default: str = None) -> str: + def get_str(self, key: str, *, default: str | None = None) -> str: """ Return the string referred to by the given key if it exists, or the default. @@ -356,7 +359,7 @@ class AbstractConfig(abc.ABC): raise NotImplementedError @abstractmethod - def get_bool(self, key: str, *, default: bool = None) -> bool: + def get_bool(self, key: str, *, default: bool | None = None) -> bool: """ Return the bool referred to by the given key if it exists, or the default. From 61bdb54eb12bdc40439bc10bd323f4811359da9f Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 09:08:03 +0000 Subject: [PATCH 07/72] .mypy.ini: Add `--explicit-package-bases` to help with pre-commit invocation --- .mypy.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/.mypy.ini b/.mypy.ini index 10ef53b7..d3a0d2e2 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -7,3 +7,4 @@ scripts_are_modules = True ; i.e. no typing info. check_untyped_defs = True ; platform = darwin +explicit_package_bases = True From 7440c9a064e8212aeffedd7eaadbd189eeebd403 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 09:08:31 +0000 Subject: [PATCH 08/72] Renamed coriolis.py so it'll never clash with plugins/coriolis.py --- coriolis.py => coriolis-update-files.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename coriolis.py => coriolis-update-files.py (100%) diff --git a/coriolis.py b/coriolis-update-files.py similarity index 100% rename from coriolis.py rename to coriolis-update-files.py From 5f4f524fec71daa0e23f73ac0f055ffbbf8c6217 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 09:10:52 +0000 Subject: [PATCH 09/72] And update reference to renamed coriolis.py in docs/Releasing.md --- docs/Releasing.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/Releasing.md b/docs/Releasing.md index 9840c694..7524ebbb 100644 --- a/docs/Releasing.md +++ b/docs/Releasing.md @@ -166,7 +166,7 @@ that it's actually included in the installer. ``` - + | None Note that you only need `Id=""` if the filename itself is not a valid Id, e.g. because it contains spaces. @@ -191,17 +191,17 @@ that it's actually included in the installer. Before you create a new install each time you should: 1. Ensure the data sourced from coriolis.io is up to date and works: - 1. Update the `coriolis-data` repo. **NB: You will need 'npm' installed for +2. Update the `coriolis-data` repo. **NB: You will need 'npm' installed for this.** - 1. `cd coriolis-data` - 1. `git pull` - 1. `npm install` - to check it's worked. - 1. Run `coriolis.py` to update `modules.p` and `ships.p`. **NB: The - submodule might have been updated by a GitHub workflow/PR/merge, so - be sure to perform this step for every build.** - 1. XXX: Test ? - 1. `git commit` the changes to the repo and the `.p` files. -1. Ensure translations are up to date, see [Translations.md](Translations.md). + 1. `cd coriolis-data` + 2. `git pull` + 3. `npm install` - to check it's worked. +3. Run `coriolis-update-files.py` to update `modules.p` and `ships.p`. **NB: + The submodule might have been updated by a GitHub workflow/PR/merge, so + be sure to perform this step for every build.** +4. XXX: Test ? +5. `git commit` the changes to the repo and the `.p` files. +6. Ensure translations are up to date, see [Translations.md](Translations.md). # Preparing to Package From d76e827dc589414a1b63f3066f4b711b04b1ae55 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 11:27:45 +0000 Subject: [PATCH 10/72] config/windows: Fix up types --- config/windows.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/config/windows.py b/config/windows.py index d29a2b42..f7590a92 100644 --- a/config/windows.py +++ b/config/windows.py @@ -8,7 +8,7 @@ import sys import uuid import winreg from ctypes.wintypes import DWORD, HANDLE -from typing import List, Optional, Union +from typing import List, Literal, Optional, Union from config import AbstractConfig, applongname, appname, logger, update_interval @@ -142,7 +142,7 @@ class WinConfig(AbstractConfig): logger.warning(f'registry key {key=} returned unknown type {_type=} {value=}') return None - def get_str(self, key: str, *, default: str = None) -> str: + def get_str(self, key: str, *, default: str | None = None) -> str: """ Return the string referred to by the given key if it exists, or the default. @@ -157,7 +157,7 @@ class WinConfig(AbstractConfig): return res - def get_list(self, key: str, *, default: list = None) -> list: + def get_list(self, key: str, *, default: list | None = None) -> list: """ Return the list referred to by the given key if it exists, or the default. @@ -187,7 +187,7 @@ class WinConfig(AbstractConfig): return res - def get_bool(self, key: str, *, default: bool = None) -> bool: + def get_bool(self, key: str, *, default: bool | None = None) -> bool: """ Return the bool referred to by the given key if it exists, or the default. @@ -205,7 +205,8 @@ class WinConfig(AbstractConfig): Implements :meth:`AbstractConfig.set`. """ - reg_type = None + # These are the types that winreg.REG_* below resolve to. + reg_type: Literal[1] | Literal[4] | Literal[7] if isinstance(val, str): reg_type = winreg.REG_SZ winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, val) From 3ed84bcad81d5a6e54246410d103caa1c9ad5cc3 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 11:30:52 +0000 Subject: [PATCH 11/72] examples/plugintest: Drop the 'pre-5.0.0' config fixups It's a pain for typing, and should be long since irrelevant. --- docs/examples/plugintest/load.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/docs/examples/plugintest/load.py b/docs/examples/plugintest/load.py index 2639cfd9..314eef19 100644 --- a/docs/examples/plugintest/load.py +++ b/docs/examples/plugintest/load.py @@ -13,19 +13,6 @@ from SubA import SubA from config import appname, appversion, config -# For compatibility with pre-5.0.0 -if not hasattr(config, 'get_int'): - config.get_int = config.getint - -if not hasattr(config, 'get_str'): - config.get_str = config.get - -if not hasattr(config, 'get_bool'): - config.get_bool = lambda key: bool(config.getint(key)) - -if not hasattr(config, 'get_list'): - config.get_list = config.get - # This could also be returned from plugin_start3() plugin_name = os.path.basename(os.path.dirname(__file__)) From 19f3df77f215e77691c163a5ab65bf70794a24b1 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 11:44:48 +0000 Subject: [PATCH 12/72] examples/click_counter: Types *should* be fixed up NB: Didn't run this at all to test. --- docs/examples/click_counter/load.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/examples/click_counter/load.py b/docs/examples/click_counter/load.py index 776e9d4b..b1ea6ac5 100644 --- a/docs/examples/click_counter/load.py +++ b/docs/examples/click_counter/load.py @@ -6,6 +6,7 @@ It adds a single button to the EDMC interface that displays the number of times import logging import tkinter as tk +import tkinter.ttk as ttk from typing import Optional import myNotebook as nb # noqa: N813 @@ -27,7 +28,7 @@ class ClickCounter: def __init__(self) -> None: # Be sure to use names that wont collide in our config variables - self.click_count: Optional[tk.StringVar] = tk.StringVar(value=str(config.get_int('click_counter_count'))) + self.click_count = tk.StringVar(value=str(config.get_int('click_counter_count'))) logger.info("ClickCounter instantiated") def on_load(self) -> str: @@ -48,7 +49,7 @@ class ClickCounter: """ self.on_preferences_closed("", False) # Save our prefs - def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: + def setup_preferences(self, parent: ttk.Frame, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: """ setup_preferences is called by plugin_prefs below. @@ -99,8 +100,8 @@ class ClickCounter: ) button.grid(row=current_row) current_row += 1 - nb.Label(frame, text="Count:").grid(row=current_row, sticky=tk.W) - nb.Label(frame, textvariable=self.click_count).grid(row=current_row, column=1) + tk.Label(frame, text="Count:").grid(row=current_row, sticky=tk.W) + tk.Label(frame, textvariable=self.click_count).grid(row=current_row, column=1) return frame @@ -127,7 +128,7 @@ def plugin_stop() -> None: return cc.on_unload() -def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: +def plugin_prefs(parent: ttk.Frame, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: """ Handle preferences tab for the plugin. From acbe1231ef871a49b318ca60a6151240d731a5f1 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 12:05:19 +0000 Subject: [PATCH 13/72] EDMCLogging: loglevel is `str | int` --- EDMCLogging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EDMCLogging.py b/EDMCLogging.py index 1bf83bc7..8bde0760 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -145,7 +145,7 @@ class Logger: logging.Logger instance. """ - def __init__(self, logger_name: str, loglevel: int = _default_loglevel): + def __init__(self, logger_name: str, loglevel: int | str = _default_loglevel): """ Set up a `logging.Logger` with our preferred configuration. @@ -541,7 +541,7 @@ def get_main_logger(sublogger_name: str = '') -> 'LoggerMixin': # Singleton -loglevel = config.get_str('loglevel') +loglevel: str | int = config.get_str('loglevel') if not loglevel: loglevel = logging.INFO From 4db416567698a0d4cb9b5e57abd1524912f2ba6b Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 12:09:34 +0000 Subject: [PATCH 14/72] config/linux: Fix `pre-commit run --all-files`-only issue When run this way mypy thinks `config.linux` has no attribute `LinuxConfig`. --- tests/config/test_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index b6637623..ad6bbd6f 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -88,7 +88,7 @@ class TestNewConfig: if sys.platform != 'linux': return - from config.linux import LinuxConfig + from config.linux import LinuxConfig # type: ignore if isinstance(config, LinuxConfig) and config.config is not None: config.config.read(config.filename) @@ -179,7 +179,7 @@ class TestOldNewConfig: if sys.platform != 'linux': return - from config.linux import LinuxConfig + from config.linux import LinuxConfig # type: ignore if isinstance(config, LinuxConfig) and config.config is not None: config.config.read(config.filename) From d40bb4f09c4b46abe99b8a21a1b2563e9d594f44 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 12:11:22 +0000 Subject: [PATCH 15/72] dashboard: Minor `process()` type fix --- dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard.py b/dashboard.py index c371e2b7..c88f9a21 100644 --- a/dashboard.py +++ b/dashboard.py @@ -167,7 +167,7 @@ class Dashboard(FileSystemEventHandler): # Can get on_modified events when the file is emptied self.process(event.src_path if not event.is_directory else None) - def process(self, logfile: str = None) -> None: + def process(self, logfile: str | None = None) -> None: """ Process the contents of current Status.json file. From f7cba59e61a2da418fe04c3c7b28d1d881b98fed Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:21:14 +0000 Subject: [PATCH 16/72] hotkey: Re-factor into a module, per-arch files This helps avoid some pre-commit/mypy carping. --- hotkey.py | 703 --------------------------------------------- hotkey/__init__.py | 59 ++++ hotkey/darwin.py | 274 ++++++++++++++++++ hotkey/linux.py | 29 ++ hotkey/windows.py | 359 +++++++++++++++++++++++ 5 files changed, 721 insertions(+), 703 deletions(-) delete mode 100644 hotkey.py create mode 100644 hotkey/__init__.py create mode 100644 hotkey/darwin.py create mode 100644 hotkey/linux.py create mode 100644 hotkey/windows.py diff --git a/hotkey.py b/hotkey.py deleted file mode 100644 index 5a72cde1..00000000 --- a/hotkey.py +++ /dev/null @@ -1,703 +0,0 @@ -"""Handle keyboard input for manual update triggering.""" -# -*- coding: utf-8 -*- - -import abc -import pathlib -import sys -import tkinter as tk -from abc import abstractmethod -from typing import Optional, Tuple, Union - -from config import config -from EDMCLogging import get_main_logger - -logger = get_main_logger() - - -class AbstractHotkeyMgr(abc.ABC): - """Abstract root class of all platforms specific HotKeyMgr.""" - - @abstractmethod - def register(self, root, keycode, modifiers) -> None: - """Register the hotkey handler.""" - pass - - @abstractmethod - def unregister(self) -> None: - """Unregister the hotkey handling.""" - pass - - @abstractmethod - def play_good(self) -> None: - """Play the 'good' sound.""" - pass - - @abstractmethod - def play_bad(self) -> None: - """Play the 'bad' sound.""" - pass - - -if sys.platform == 'darwin': - - import objc - from AppKit import ( - NSAlternateKeyMask, NSApplication, NSBeep, NSClearLineFunctionKey, NSCommandKeyMask, NSControlKeyMask, - NSDeleteFunctionKey, NSDeviceIndependentModifierFlagsMask, NSEvent, NSF1FunctionKey, NSF35FunctionKey, - NSFlagsChanged, NSKeyDown, NSKeyDownMask, NSKeyUp, NSNumericPadKeyMask, NSShiftKeyMask, NSSound, NSWorkspace - ) - - -class MacHotkeyMgr(AbstractHotkeyMgr): - """Hot key management.""" - - POLL = 250 - # https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/#//apple_ref/doc/constant_group/Function_Key_Unicodes - DISPLAY = { - 0x03: u'⌅', 0x09: u'⇥', 0xd: u'↩', 0x19: u'⇤', 0x1b: u'esc', 0x20: u'⏘', 0x7f: u'⌫', - 0xf700: u'↑', 0xf701: u'↓', 0xf702: u'←', 0xf703: u'→', - 0xf727: u'Ins', - 0xf728: u'⌦', 0xf729: u'↖', 0xf72a: u'Fn', 0xf72b: u'↘', - 0xf72c: u'⇞', 0xf72d: u'⇟', 0xf72e: u'PrtScr', 0xf72f: u'ScrollLock', - 0xf730: u'Pause', 0xf731: u'SysReq', 0xf732: u'Break', 0xf733: u'Reset', - 0xf739: u'⌧', - } - (ACQUIRE_INACTIVE, ACQUIRE_ACTIVE, ACQUIRE_NEW) = range(3) - - def __init__(self): - self.MODIFIERMASK = NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask \ - | NSNumericPadKeyMask - self.root = None - - self.keycode = 0 - self.modifiers = 0 - self.activated = False - self.observer = None - - self.acquire_key = 0 - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - - self.tkProcessKeyEvent_old = None - - self.snd_good = NSSound.alloc().initWithContentsOfFile_byReference_( - pathlib.Path(config.respath_path) / 'snd_good.wav', False - ) - self.snd_bad = NSSound.alloc().initWithContentsOfFile_byReference_( - pathlib.Path(config.respath_path) / 'snd_bad.wav', False - ) - - def register(self, root: tk.Tk, keycode, modifiers) -> None: - """ - Register current hotkey for monitoring. - - :param root: parent window. - :param keycode: Key to monitor. - :param modifiers: Any modifiers to take into account. - """ - self.root = root - self.keycode = keycode - self.modifiers = modifiers - self.activated = False - - if keycode: - if not self.observer: - self.root.after_idle(self._observe) - self.root.after(MacHotkeyMgr.POLL, self._poll) - - # Monkey-patch tk (tkMacOSXKeyEvent.c) - if not self.tkProcessKeyEvent_old: - sel = b'tkProcessKeyEvent:' - cls = NSApplication.sharedApplication().class__() # type: ignore - self.tkProcessKeyEvent_old = NSApplication.sharedApplication().methodForSelector_(sel) # type: ignore - newmethod = objc.selector( # type: ignore - self.tkProcessKeyEvent, - selector=self.tkProcessKeyEvent_old.selector, - signature=self.tkProcessKeyEvent_old.signature - ) - objc.classAddMethod(cls, sel, newmethod) # type: ignore - - def tkProcessKeyEvent(self, cls, the_event): # noqa: N802 - """ - Monkey-patch tk (tkMacOSXKeyEvent.c). - - - workaround crash on OSX 10.9 & 10.10 on seeing a composing character - - notice when modifier key state changes - - keep a copy of NSEvent.charactersIgnoringModifiers, which is what we need for the hotkey - - (Would like to use a decorator but need to ensure the application is created before this is installed) - :param cls: ??? - :param the_event: tk event - :return: ??? - """ - if self.acquire_state: - if the_event.type() == NSFlagsChanged: - self.acquire_key = the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask - self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW - # suppress the event by not chaining the old function - return the_event - - elif the_event.type() in (NSKeyDown, NSKeyUp): - c = the_event.charactersIgnoringModifiers() - self.acquire_key = (c and ord(c[0]) or 0) | \ - (the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask) - self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW - # suppress the event by not chaining the old function - return the_event - - # replace empty characters with charactersIgnoringModifiers to avoid crash - elif the_event.type() in (NSKeyDown, NSKeyUp) and not the_event.characters(): - the_event = NSEvent.keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode_( # noqa: E501 - # noqa: E501 - the_event.type(), - the_event.locationInWindow(), - the_event.modifierFlags(), - the_event.timestamp(), - the_event.windowNumber(), - the_event.context(), - the_event.charactersIgnoringModifiers(), - the_event.charactersIgnoringModifiers(), - the_event.isARepeat(), - the_event.keyCode() - ) - return self.tkProcessKeyEvent_old(cls, the_event) - - def _observe(self): - # Must be called after root.mainloop() so that the app's message loop has been created - self.observer = NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSKeyDownMask, self._handler) - - def _poll(self): - if config.shutting_down: - return - - # No way of signalling to Tkinter from within the callback handler block that doesn't - # cause Python to crash, so poll. - if self.activated: - self.activated = False - self.root.event_generate('<>', when="tail") - - if self.keycode or self.modifiers: - self.root.after(MacHotkeyMgr.POLL, self._poll) - - def unregister(self) -> None: - """Remove hotkey registration.""" - self.keycode = None - self.modifiers = None - - if sys.platform == 'darwin': # noqa: C901 - @objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_) - def _handler(self, event) -> None: - # use event.charactersIgnoringModifiers to handle composing characters like Alt-e - if ((event.modifierFlags() & self.MODIFIERMASK) == self.modifiers - and ord(event.charactersIgnoringModifiers()[0]) == self.keycode): - if config.get_int('hotkey_always'): - self.activated = True - - else: # Only trigger if game client is front process - front = NSWorkspace.sharedWorkspace().frontmostApplication() - if front and front.bundleIdentifier() == 'uk.co.frontier.EliteDangerous': - self.activated = True - - def acquire_start(self) -> None: - """Start acquiring hotkey state via polling.""" - self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE - self.root.after_idle(self._acquire_poll) - - def acquire_stop(self) -> None: - """Stop acquiring hotkey state.""" - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - - def _acquire_poll(self) -> None: - """Perform a poll of current hotkey state.""" - if config.shutting_down: - return - - # No way of signalling to Tkinter from within the monkey-patched event handler that doesn't - # cause Python to crash, so poll. - if self.acquire_state: - if self.acquire_state == MacHotkeyMgr.ACQUIRE_NEW: - # Abuse tkEvent's keycode field to hold our acquired key & modifier - self.root.event_generate('', keycode=self.acquire_key) - self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE - self.root.after(50, self._acquire_poll) - - def fromevent(self, event) -> Optional[Union[bool, Tuple]]: - """ - Return configuration (keycode, modifiers) or None=clear or False=retain previous. - - :param event: tk event ? - :return: False to retain previous, None to not use, else (keycode, modifiers) - """ - (keycode, modifiers) = (event.keycode & 0xffff, event.keycode & 0xffff0000) # Set by _acquire_poll() - if (keycode - and not (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask))): - if keycode == 0x1b: # Esc = retain previous - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - return False - - # BkSp, Del, Clear = clear hotkey - elif keycode in [0x7f, ord(NSDeleteFunctionKey), ord(NSClearLineFunctionKey)]: - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - return None - - # don't allow keys needed for typing in System Map - elif keycode in [0x13, 0x20, 0x2d] or 0x61 <= keycode <= 0x7a: - NSBeep() - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - return None - - return (keycode, modifiers) - - def display(self, keycode, modifiers) -> str: - """ - Return displayable form of given hotkey + modifiers. - - :param keycode: - :param modifiers: - :return: string form - """ - text = '' - if modifiers & NSControlKeyMask: - text += u'⌃' - - if modifiers & NSAlternateKeyMask: - text += u'⌥' - - if modifiers & NSShiftKeyMask: - text += u'⇧' - - if modifiers & NSCommandKeyMask: - text += u'⌘' - - if (modifiers & NSNumericPadKeyMask) and keycode <= 0x7f: - text += u'№' - - if not keycode: - pass - - elif ord(NSF1FunctionKey) <= keycode <= ord(NSF35FunctionKey): - text += f'F{keycode + 1 - ord(NSF1FunctionKey)}' - - elif keycode in MacHotkeyMgr.DISPLAY: # specials - text += MacHotkeyMgr.DISPLAY[keycode] - - elif keycode < 0x20: # control keys - text += chr(keycode + 0x40) - - elif keycode < 0xf700: # key char - text += chr(keycode).upper() - - else: - text += u'⁈' - - return text - - def play_good(self): - """Play the 'good' sound.""" - self.snd_good.play() - - def play_bad(self): - """Play the 'bad' sound.""" - self.snd_bad.play() - - -if sys.platform == 'win32': - - import atexit - import ctypes - import threading - import winsound - from ctypes.wintypes import DWORD, HWND, LONG, LPWSTR, MSG, ULONG, WORD - - RegisterHotKey = ctypes.windll.user32.RegisterHotKey - UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey - MOD_ALT = 0x0001 - MOD_CONTROL = 0x0002 - MOD_SHIFT = 0x0004 - MOD_WIN = 0x0008 - MOD_NOREPEAT = 0x4000 - - GetMessage = ctypes.windll.user32.GetMessageW - TranslateMessage = ctypes.windll.user32.TranslateMessage - DispatchMessage = ctypes.windll.user32.DispatchMessageW - PostThreadMessage = ctypes.windll.user32.PostThreadMessageW - WM_QUIT = 0x0012 - WM_HOTKEY = 0x0312 - WM_APP = 0x8000 - WM_SND_GOOD = WM_APP + 1 - WM_SND_BAD = WM_APP + 2 - - GetKeyState = ctypes.windll.user32.GetKeyState - MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW - VK_BACK = 0x08 - VK_CLEAR = 0x0c - VK_RETURN = 0x0d - VK_SHIFT = 0x10 - VK_CONTROL = 0x11 - VK_MENU = 0x12 - VK_CAPITAL = 0x14 - VK_MODECHANGE = 0x1f - VK_ESCAPE = 0x1b - VK_SPACE = 0x20 - VK_DELETE = 0x2e - VK_LWIN = 0x5b - VK_RWIN = 0x5c - VK_NUMPAD0 = 0x60 - VK_DIVIDE = 0x6f - VK_F1 = 0x70 - VK_F24 = 0x87 - VK_OEM_MINUS = 0xbd - VK_NUMLOCK = 0x90 - VK_SCROLL = 0x91 - VK_PROCESSKEY = 0xe5 - VK_OEM_CLEAR = 0xfe - - GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow - GetWindowText = ctypes.windll.user32.GetWindowTextW - GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] - GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW - - def window_title(h) -> str: - """ - Determine the title for a window. - - :param h: Window handle. - :return: Window title. - """ - if h: - title_length = GetWindowTextLength(h) + 1 - buf = ctypes.create_unicode_buffer(title_length) - if GetWindowText(h, buf, title_length): - return buf.value - - return '' - - class MOUSEINPUT(ctypes.Structure): - """Mouse Input structure.""" - - _fields_ = [ - ('dx', LONG), - ('dy', LONG), - ('mouseData', DWORD), - ('dwFlags', DWORD), - ('time', DWORD), - ('dwExtraInfo', ctypes.POINTER(ULONG)) - ] - - class KEYBDINPUT(ctypes.Structure): - """Keyboard Input structure.""" - - _fields_ = [ - ('wVk', WORD), - ('wScan', WORD), - ('dwFlags', DWORD), - ('time', DWORD), - ('dwExtraInfo', ctypes.POINTER(ULONG)) - ] - - class HARDWAREINPUT(ctypes.Structure): - """Hardware Input structure.""" - - _fields_ = [ - ('uMsg', DWORD), - ('wParamL', WORD), - ('wParamH', WORD) - ] - - class INPUTUNION(ctypes.Union): - """Input union.""" - - _fields_ = [ - ('mi', MOUSEINPUT), - ('ki', KEYBDINPUT), - ('hi', HARDWAREINPUT) - ] - - class INPUT(ctypes.Structure): - """Input structure.""" - - _fields_ = [ - ('type', DWORD), - ('union', INPUTUNION) - ] - - SendInput = ctypes.windll.user32.SendInput - SendInput.argtypes = [ctypes.c_uint, ctypes.POINTER(INPUT), ctypes.c_int] - - INPUT_MOUSE = 0 - INPUT_KEYBOARD = 1 - INPUT_HARDWARE = 2 - - -class WindowsHotkeyMgr(AbstractHotkeyMgr): - """Hot key management.""" - - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx - # Limit ourselves to symbols in Windows 7 Segoe UI - DISPLAY = { - 0x03: 'Break', 0x08: 'Bksp', 0x09: u'↹', 0x0c: 'Clear', 0x0d: u'↵', 0x13: 'Pause', - 0x14: u'Ⓐ', 0x1b: 'Esc', - 0x20: u'⏘', 0x21: 'PgUp', 0x22: 'PgDn', 0x23: 'End', 0x24: 'Home', - 0x25: u'←', 0x26: u'↑', 0x27: u'→', 0x28: u'↓', - 0x2c: 'PrtScn', 0x2d: 'Ins', 0x2e: 'Del', 0x2f: 'Help', - 0x5d: u'▤', 0x5f: u'☾', - 0x90: u'➀', 0x91: 'ScrLk', - 0xa6: u'⇦', 0xa7: u'⇨', 0xa9: u'⊗', 0xab: u'☆', 0xac: u'⌂', 0xb4: u'✉', - } - - def __init__(self) -> None: - self.root: tk.Tk = None # type: ignore - self.thread: threading.Thread = None # type: ignore - with open(pathlib.Path(config.respath) / 'snd_good.wav', 'rb') as sg: - self.snd_good = sg.read() - with open(pathlib.Path(config.respath) / 'snd_bad.wav', 'rb') as sb: - self.snd_bad = sb.read() - atexit.register(self.unregister) - - def register(self, root: tk.Tk, keycode, modifiers) -> None: - """Register the hotkey handler.""" - self.root = root - - if self.thread: - logger.debug('Was already registered, unregistering...') - self.unregister() - - if keycode or modifiers: - logger.debug('Creating thread worker...') - self.thread = threading.Thread( - target=self.worker, - name=f'Hotkey "{keycode}:{modifiers}"', - args=(keycode, modifiers) - ) - self.thread.daemon = True - logger.debug('Starting thread worker...') - self.thread.start() - logger.debug('Done.') - - def unregister(self) -> None: - """Unregister the hotkey handling.""" - thread = self.thread - - if thread: - logger.debug('Thread is/was running') - self.thread = None # type: ignore - logger.debug('Telling thread WM_QUIT') - PostThreadMessage(thread.ident, WM_QUIT, 0, 0) - logger.debug('Joining thread') - thread.join() # Wait for it to unregister hotkey and quit - - else: - logger.debug('No thread') - - logger.debug('Done.') - - def worker(self, keycode, modifiers) -> None: # noqa: CCR001 - """Handle hotkeys.""" - logger.debug('Begin...') - # Hotkey must be registered by the thread that handles it - if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): - logger.debug("We're not the right thread?") - self.thread = None # type: ignore - return - - fake = INPUT(INPUT_KEYBOARD, INPUTUNION(ki=KEYBDINPUT(keycode, keycode, 0, 0, None))) - - msg = MSG() - logger.debug('Entering GetMessage() loop...') - while GetMessage(ctypes.byref(msg), None, 0, 0) != 0: - logger.debug('Got message') - if msg.message == WM_HOTKEY: - logger.debug('WM_HOTKEY') - - if ( - config.get_int('hotkey_always') - or window_title(GetForegroundWindow()).startswith('Elite - Dangerous') - ): - if not config.shutting_down: - logger.debug('Sending event <>') - self.root.event_generate('<>', when="tail") - - else: - logger.debug('Passing key on') - UnregisterHotKey(None, 1) - SendInput(1, fake, ctypes.sizeof(INPUT)) - if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): - logger.debug("We aren't registered for this ?") - break - - elif msg.message == WM_SND_GOOD: - logger.debug('WM_SND_GOOD') - winsound.PlaySound(self.snd_good, winsound.SND_MEMORY) # synchronous - - elif msg.message == WM_SND_BAD: - logger.debug('WM_SND_BAD') - winsound.PlaySound(self.snd_bad, winsound.SND_MEMORY) # synchronous - - else: - logger.debug('Something else') - TranslateMessage(ctypes.byref(msg)) - DispatchMessage(ctypes.byref(msg)) - - logger.debug('Exited GetMessage() loop.') - UnregisterHotKey(None, 1) - self.thread = None # type: ignore - logger.debug('Done.') - - def acquire_start(self) -> None: - """Start acquiring hotkey state via polling.""" - pass - - def acquire_stop(self) -> None: - """Stop acquiring hotkey state.""" - pass - - def fromevent(self, event) -> Optional[Union[bool, Tuple]]: # noqa: CCR001 - """ - Return configuration (keycode, modifiers) or None=clear or False=retain previous. - - event.state is a pain - it shows the state of the modifiers *before* a modifier key was pressed. - event.state *does* differentiate between left and right Ctrl and Alt and between Return and Enter - by putting KF_EXTENDED in bit 18, but RegisterHotKey doesn't differentiate. - - :param event: tk event ? - :return: False to retain previous, None to not use, else (keycode, modifiers) - """ - modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ - | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ - | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ - | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ - | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) - keycode = event.keycode - - if keycode in [VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN]: - return (0, modifiers) - - if not modifiers: - if keycode == VK_ESCAPE: # Esc = retain previous - return False - - elif keycode in [VK_BACK, VK_DELETE, VK_CLEAR, VK_OEM_CLEAR]: # BkSp, Del, Clear = clear hotkey - return None - - elif keycode in [VK_RETURN, VK_SPACE, VK_OEM_MINUS] or ord('A') <= keycode <= ord( - 'Z'): # don't allow keys needed for typing in System Map - winsound.MessageBeep() - return None - - elif (keycode in [VK_NUMLOCK, VK_SCROLL, VK_PROCESSKEY] - or VK_CAPITAL <= keycode <= VK_MODECHANGE): # ignore unmodified mode switch keys - return (0, modifiers) - - # See if the keycode is usable and available - if RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode): - UnregisterHotKey(None, 2) - return (keycode, modifiers) - - else: - winsound.MessageBeep() - return None - - def display(self, keycode, modifiers) -> str: - """ - Return displayable form of given hotkey + modifiers. - - :param keycode: - :param modifiers: - :return: string form - """ - text = '' - if modifiers & MOD_WIN: - text += u'❖+' - - if modifiers & MOD_CONTROL: - text += u'Ctrl+' - - if modifiers & MOD_ALT: - text += u'Alt+' - - if modifiers & MOD_SHIFT: - text += u'⇧+' - - if VK_NUMPAD0 <= keycode <= VK_DIVIDE: - text += u'№' - - if not keycode: - pass - - elif VK_F1 <= keycode <= VK_F24: - text += f'F{keycode + 1 - VK_F1}' - - elif keycode in WindowsHotkeyMgr.DISPLAY: # specials - text += WindowsHotkeyMgr.DISPLAY[keycode] - - else: - c = MapVirtualKey(keycode, 2) # printable ? - if not c: # oops not printable - text += u'⁈' - - elif c < 0x20: # control keys - text += chr(c + 0x40) - - else: - text += chr(c).upper() - - return text - - def play_good(self) -> None: - """Play the 'good' sound.""" - if self.thread: - PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) - - def play_bad(self) -> None: - """Play the 'bad' sound.""" - if self.thread: - PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) - - -class LinuxHotKeyMgr(AbstractHotkeyMgr): - """ - Hot key management. - - Not actually implemented on Linux. It's a no-op instead. - """ - - def register(self, root, keycode, modifiers) -> None: - """Register the hotkey handler.""" - pass - - def unregister(self) -> None: - """Unregister the hotkey handling.""" - pass - - def play_good(self) -> None: - """Play the 'good' sound.""" - pass - - def play_bad(self) -> None: - """Play the 'bad' sound.""" - pass - - -def get_hotkeymgr() -> AbstractHotkeyMgr: - """ - Determine platform-specific HotkeyMgr. - - :param args: - :param kwargs: - :return: Appropriate class instance. - :raises ValueError: If unsupported platform. - """ - if sys.platform == 'darwin': - return MacHotkeyMgr() - - elif sys.platform == 'win32': - return WindowsHotkeyMgr() - - elif sys.platform == 'linux': - return LinuxHotKeyMgr() - - else: - raise ValueError(f'Unknown platform: {sys.platform}') - - -# singleton -hotkeymgr = get_hotkeymgr() diff --git a/hotkey/__init__.py b/hotkey/__init__.py new file mode 100644 index 00000000..782a16fa --- /dev/null +++ b/hotkey/__init__.py @@ -0,0 +1,59 @@ +"""Handle keyboard input for manual update triggering.""" +# -*- coding: utf-8 -*- + +import abc +import sys +from abc import abstractmethod + + +class AbstractHotkeyMgr(abc.ABC): + """Abstract root class of all platforms specific HotKeyMgr.""" + + @abstractmethod + def register(self, root, keycode, modifiers) -> None: + """Register the hotkey handler.""" + pass + + @abstractmethod + def unregister(self) -> None: + """Unregister the hotkey handling.""" + pass + + @abstractmethod + def play_good(self) -> None: + """Play the 'good' sound.""" + pass + + @abstractmethod + def play_bad(self) -> None: + """Play the 'bad' sound.""" + pass + + +def get_hotkeymgr() -> AbstractHotkeyMgr: + """ + Determine platform-specific HotkeyMgr. + + :param args: + :param kwargs: + :return: Appropriate class instance. + :raises ValueError: If unsupported platform. + """ + if sys.platform == 'darwin': + from hotkey.darwin import MacHotkeyMgr + return MacHotkeyMgr() + + elif sys.platform == 'win32': + from hotkey.windows import WindowsHotkeyMgr + return WindowsHotkeyMgr() + + elif sys.platform == 'linux': + from hotkey.linux import LinuxHotKeyMgr + return LinuxHotKeyMgr() + + else: + raise ValueError(f'Unknown platform: {sys.platform}') + + +# singleton +hotkeymgr = get_hotkeymgr() diff --git a/hotkey/darwin.py b/hotkey/darwin.py new file mode 100644 index 00000000..9989b4f9 --- /dev/null +++ b/hotkey/darwin.py @@ -0,0 +1,274 @@ +"""darwin/macOS implementation of hotkey.AbstractHotkeyMgr.""" +import pathlib +import sys +import tkinter as tk +from typing import Callable, Optional, Tuple, Union + +import objc +from AppKit import ( + NSAlternateKeyMask, NSApplication, NSBeep, NSClearLineFunctionKey, NSCommandKeyMask, NSControlKeyMask, + NSDeleteFunctionKey, NSDeviceIndependentModifierFlagsMask, NSEvent, NSF1FunctionKey, NSF35FunctionKey, + NSFlagsChanged, NSKeyDown, NSKeyDownMask, NSKeyUp, NSNumericPadKeyMask, NSShiftKeyMask, NSSound, NSWorkspace +) + +from config import config +from EDMCLogging import get_main_logger +from hotkey import AbstractHotkeyMgr + +logger = get_main_logger() + + +class MacHotkeyMgr(AbstractHotkeyMgr): + """Hot key management.""" + + POLL = 250 + # https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/#//apple_ref/doc/constant_group/Function_Key_Unicodes + DISPLAY = { + 0x03: u'⌅', 0x09: u'⇥', 0xd: u'↩', 0x19: u'⇤', 0x1b: u'esc', 0x20: u'⏘', 0x7f: u'⌫', + 0xf700: u'↑', 0xf701: u'↓', 0xf702: u'←', 0xf703: u'→', + 0xf727: u'Ins', + 0xf728: u'⌦', 0xf729: u'↖', 0xf72a: u'Fn', 0xf72b: u'↘', + 0xf72c: u'⇞', 0xf72d: u'⇟', 0xf72e: u'PrtScr', 0xf72f: u'ScrollLock', + 0xf730: u'Pause', 0xf731: u'SysReq', 0xf732: u'Break', 0xf733: u'Reset', + 0xf739: u'⌧', + } + (ACQUIRE_INACTIVE, ACQUIRE_ACTIVE, ACQUIRE_NEW) = range(3) + + def __init__(self): + self.MODIFIERMASK = NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask \ + | NSNumericPadKeyMask + self.root: tk.Tk + + self.keycode = 0 + self.modifiers = 0 + self.activated = False + self.observer = None + + self.acquire_key = 0 + self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE + + self.tkProcessKeyEvent_old: Callable + + self.snd_good = NSSound.alloc().initWithContentsOfFile_byReference_( + pathlib.Path(config.respath_path) / 'snd_good.wav', False + ) + self.snd_bad = NSSound.alloc().initWithContentsOfFile_byReference_( + pathlib.Path(config.respath_path) / 'snd_bad.wav', False + ) + + def register(self, root: tk.Tk, keycode, modifiers) -> None: + """ + Register current hotkey for monitoring. + + :param root: parent window. + :param keycode: Key to monitor. + :param modifiers: Any modifiers to take into account. + """ + self.root = root + self.keycode = keycode + self.modifiers = modifiers + self.activated = False + + if keycode: + if not self.observer: + self.root.after_idle(self._observe) + self.root.after(MacHotkeyMgr.POLL, self._poll) + + # Monkey-patch tk (tkMacOSXKeyEvent.c) + if not callable(self.tkProcessKeyEvent_old): + sel = b'tkProcessKeyEvent:' + cls = NSApplication.sharedApplication().class__() # type: ignore + self.tkProcessKeyEvent_old = NSApplication.sharedApplication().methodForSelector_(sel) # type: ignore + newmethod = objc.selector( # type: ignore + self.tkProcessKeyEvent, + selector=self.tkProcessKeyEvent_old.selector, + signature=self.tkProcessKeyEvent_old.signature + ) + objc.classAddMethod(cls, sel, newmethod) # type: ignore + + def tkProcessKeyEvent(self, cls, the_event): # noqa: N802 + """ + Monkey-patch tk (tkMacOSXKeyEvent.c). + + - workaround crash on OSX 10.9 & 10.10 on seeing a composing character + - notice when modifier key state changes + - keep a copy of NSEvent.charactersIgnoringModifiers, which is what we need for the hotkey + + (Would like to use a decorator but need to ensure the application is created before this is installed) + :param cls: ??? + :param the_event: tk event + :return: ??? + """ + if self.acquire_state: + if the_event.type() == NSFlagsChanged: + self.acquire_key = the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask + self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW + # suppress the event by not chaining the old function + return the_event + + elif the_event.type() in (NSKeyDown, NSKeyUp): + c = the_event.charactersIgnoringModifiers() + self.acquire_key = (c and ord(c[0]) or 0) | \ + (the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask) + self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW + # suppress the event by not chaining the old function + return the_event + + # replace empty characters with charactersIgnoringModifiers to avoid crash + elif the_event.type() in (NSKeyDown, NSKeyUp) and not the_event.characters(): + the_event = NSEvent.keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode_( # noqa: E501 + # noqa: E501 + the_event.type(), + the_event.locationInWindow(), + the_event.modifierFlags(), + the_event.timestamp(), + the_event.windowNumber(), + the_event.context(), + the_event.charactersIgnoringModifiers(), + the_event.charactersIgnoringModifiers(), + the_event.isARepeat(), + the_event.keyCode() + ) + return self.tkProcessKeyEvent_old(cls, the_event) + + def _observe(self): + # Must be called after root.mainloop() so that the app's message loop has been created + self.observer = NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSKeyDownMask, self._handler) + + def _poll(self): + if config.shutting_down: + return + + # No way of signalling to Tkinter from within the callback handler block that doesn't + # cause Python to crash, so poll. + if self.activated: + self.activated = False + self.root.event_generate('<>', when="tail") + + if self.keycode or self.modifiers: + self.root.after(MacHotkeyMgr.POLL, self._poll) + + def unregister(self) -> None: + """Remove hotkey registration.""" + self.keycode = 0 + self.modifiers = 0 + + if sys.platform == 'darwin': # noqa: C901 + @objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_) + def _handler(self, event) -> None: + # use event.charactersIgnoringModifiers to handle composing characters like Alt-e + if ( + (event.modifierFlags() & self.MODIFIERMASK) == self.modifiers + and ord(event.charactersIgnoringModifiers()[0]) == self.keycode + ): + if config.get_int('hotkey_always'): + self.activated = True + + else: # Only trigger if game client is front process + front = NSWorkspace.sharedWorkspace().frontmostApplication() + if front and front.bundleIdentifier() == 'uk.co.frontier.EliteDangerous': + self.activated = True + + def acquire_start(self) -> None: + """Start acquiring hotkey state via polling.""" + self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE + self.root.after_idle(self._acquire_poll) + + def acquire_stop(self) -> None: + """Stop acquiring hotkey state.""" + self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE + + def _acquire_poll(self) -> None: + """Perform a poll of current hotkey state.""" + if config.shutting_down: + return + + # No way of signalling to Tkinter from within the monkey-patched event handler that doesn't + # cause Python to crash, so poll. + if self.acquire_state: + if self.acquire_state == MacHotkeyMgr.ACQUIRE_NEW: + # Abuse tkEvent's keycode field to hold our acquired key & modifier + self.root.event_generate('', keycode=self.acquire_key) + self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE + self.root.after(50, self._acquire_poll) + + def fromevent(self, event) -> Optional[Union[bool, Tuple]]: + """ + Return configuration (keycode, modifiers) or None=clear or False=retain previous. + + :param event: tk event ? + :return: False to retain previous, None to not use, else (keycode, modifiers) + """ + (keycode, modifiers) = (event.keycode & 0xffff, event.keycode & 0xffff0000) # Set by _acquire_poll() + if ( + keycode + and not (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask)) + ): + if keycode == 0x1b: # Esc = retain previous + self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE + return False + + # BkSp, Del, Clear = clear hotkey + elif keycode in [0x7f, ord(NSDeleteFunctionKey), ord(NSClearLineFunctionKey)]: + self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE + return None + + # don't allow keys needed for typing in System Map + elif keycode in [0x13, 0x20, 0x2d] or 0x61 <= keycode <= 0x7a: + NSBeep() + self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE + return None + + return (keycode, modifiers) + + def display(self, keycode, modifiers) -> str: + """ + Return displayable form of given hotkey + modifiers. + + :param keycode: + :param modifiers: + :return: string form + """ + text = '' + if modifiers & NSControlKeyMask: + text += u'⌃' + + if modifiers & NSAlternateKeyMask: + text += u'⌥' + + if modifiers & NSShiftKeyMask: + text += u'⇧' + + if modifiers & NSCommandKeyMask: + text += u'⌘' + + if (modifiers & NSNumericPadKeyMask) and keycode <= 0x7f: + text += u'№' + + if not keycode: + pass + + elif ord(NSF1FunctionKey) <= keycode <= ord(NSF35FunctionKey): + text += f'F{keycode + 1 - ord(NSF1FunctionKey)}' + + elif keycode in MacHotkeyMgr.DISPLAY: # specials + text += MacHotkeyMgr.DISPLAY[keycode] + + elif keycode < 0x20: # control keys + text += chr(keycode + 0x40) + + elif keycode < 0xf700: # key char + text += chr(keycode).upper() + + else: + text += u'⁈' + + return text + + def play_good(self): + """Play the 'good' sound.""" + self.snd_good.play() + + def play_bad(self): + """Play the 'bad' sound.""" + self.snd_bad.play() diff --git a/hotkey/linux.py b/hotkey/linux.py new file mode 100644 index 00000000..9a60515f --- /dev/null +++ b/hotkey/linux.py @@ -0,0 +1,29 @@ +"""Linux implementation of hotkey.AbstractHotkeyMgr.""" +from EDMCLogging import get_main_logger +from hotkey import AbstractHotkeyMgr + +logger = get_main_logger() + + +class LinuxHotKeyMgr(AbstractHotkeyMgr): + """ + Hot key management. + + Not actually implemented on Linux. It's a no-op instead. + """ + + def register(self, root, keycode, modifiers) -> None: + """Register the hotkey handler.""" + pass + + def unregister(self) -> None: + """Unregister the hotkey handling.""" + pass + + def play_good(self) -> None: + """Play the 'good' sound.""" + pass + + def play_bad(self) -> None: + """Play the 'bad' sound.""" + pass diff --git a/hotkey/windows.py b/hotkey/windows.py new file mode 100644 index 00000000..4dd110af --- /dev/null +++ b/hotkey/windows.py @@ -0,0 +1,359 @@ +"""Windows implementation of hotkey.AbstractHotkeyMgr.""" +import atexit +import ctypes +import pathlib +import threading +import tkinter as tk +import winsound +from ctypes.wintypes import DWORD, HWND, LONG, LPWSTR, MSG, ULONG, WORD +from typing import Optional, Tuple, Union + +from config import config +from EDMCLogging import get_main_logger +from hotkey import AbstractHotkeyMgr + +logger = get_main_logger() + +RegisterHotKey = ctypes.windll.user32.RegisterHotKey +UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey +MOD_ALT = 0x0001 +MOD_CONTROL = 0x0002 +MOD_SHIFT = 0x0004 +MOD_WIN = 0x0008 +MOD_NOREPEAT = 0x4000 + +GetMessage = ctypes.windll.user32.GetMessageW +TranslateMessage = ctypes.windll.user32.TranslateMessage +DispatchMessage = ctypes.windll.user32.DispatchMessageW +PostThreadMessage = ctypes.windll.user32.PostThreadMessageW +WM_QUIT = 0x0012 +WM_HOTKEY = 0x0312 +WM_APP = 0x8000 +WM_SND_GOOD = WM_APP + 1 +WM_SND_BAD = WM_APP + 2 + +GetKeyState = ctypes.windll.user32.GetKeyState +MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW +VK_BACK = 0x08 +VK_CLEAR = 0x0c +VK_RETURN = 0x0d +VK_SHIFT = 0x10 +VK_CONTROL = 0x11 +VK_MENU = 0x12 +VK_CAPITAL = 0x14 +VK_MODECHANGE = 0x1f +VK_ESCAPE = 0x1b +VK_SPACE = 0x20 +VK_DELETE = 0x2e +VK_LWIN = 0x5b +VK_RWIN = 0x5c +VK_NUMPAD0 = 0x60 +VK_DIVIDE = 0x6f +VK_F1 = 0x70 +VK_F24 = 0x87 +VK_OEM_MINUS = 0xbd +VK_NUMLOCK = 0x90 +VK_SCROLL = 0x91 +VK_PROCESSKEY = 0xe5 +VK_OEM_CLEAR = 0xfe + +GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow +GetWindowText = ctypes.windll.user32.GetWindowTextW +GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] +GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW + +def window_title(h) -> str: + """ + Determine the title for a window. + + :param h: Window handle. + :return: Window title. + """ + if h: + title_length = GetWindowTextLength(h) + 1 + buf = ctypes.create_unicode_buffer(title_length) + if GetWindowText(h, buf, title_length): + return buf.value + + return '' + +class MOUSEINPUT(ctypes.Structure): + """Mouse Input structure.""" + + _fields_ = [ + ('dx', LONG), + ('dy', LONG), + ('mouseData', DWORD), + ('dwFlags', DWORD), + ('time', DWORD), + ('dwExtraInfo', ctypes.POINTER(ULONG)) + ] + +class KEYBDINPUT(ctypes.Structure): + """Keyboard Input structure.""" + + _fields_ = [ + ('wVk', WORD), + ('wScan', WORD), + ('dwFlags', DWORD), + ('time', DWORD), + ('dwExtraInfo', ctypes.POINTER(ULONG)) + ] + +class HARDWAREINPUT(ctypes.Structure): + """Hardware Input structure.""" + + _fields_ = [ + ('uMsg', DWORD), + ('wParamL', WORD), + ('wParamH', WORD) + ] + +class INPUTUNION(ctypes.Union): + """Input union.""" + + _fields_ = [ + ('mi', MOUSEINPUT), + ('ki', KEYBDINPUT), + ('hi', HARDWAREINPUT) + ] + +class INPUT(ctypes.Structure): + """Input structure.""" + + _fields_ = [ + ('type', DWORD), + ('union', INPUTUNION) + ] + +SendInput = ctypes.windll.user32.SendInput +SendInput.argtypes = [ctypes.c_uint, ctypes.POINTER(INPUT), ctypes.c_int] + +INPUT_MOUSE = 0 +INPUT_KEYBOARD = 1 +INPUT_HARDWARE = 2 + + +class WindowsHotkeyMgr(AbstractHotkeyMgr): + """Hot key management.""" + + # https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx + # Limit ourselves to symbols in Windows 7 Segoe UI + DISPLAY = { + 0x03: 'Break', 0x08: 'Bksp', 0x09: u'↹', 0x0c: 'Clear', 0x0d: u'↵', 0x13: 'Pause', + 0x14: u'Ⓐ', 0x1b: 'Esc', + 0x20: u'⏘', 0x21: 'PgUp', 0x22: 'PgDn', 0x23: 'End', 0x24: 'Home', + 0x25: u'←', 0x26: u'↑', 0x27: u'→', 0x28: u'↓', + 0x2c: 'PrtScn', 0x2d: 'Ins', 0x2e: 'Del', 0x2f: 'Help', + 0x5d: u'▤', 0x5f: u'☾', + 0x90: u'➀', 0x91: 'ScrLk', + 0xa6: u'⇦', 0xa7: u'⇨', 0xa9: u'⊗', 0xab: u'☆', 0xac: u'⌂', 0xb4: u'✉', + } + + def __init__(self) -> None: + self.root: tk.Tk = None # type: ignore + self.thread: threading.Thread = None # type: ignore + with open(pathlib.Path(config.respath) / 'snd_good.wav', 'rb') as sg: + self.snd_good = sg.read() + with open(pathlib.Path(config.respath) / 'snd_bad.wav', 'rb') as sb: + self.snd_bad = sb.read() + atexit.register(self.unregister) + + def register(self, root: tk.Tk, keycode, modifiers) -> None: + """Register the hotkey handler.""" + self.root = root + + if self.thread: + logger.debug('Was already registered, unregistering...') + self.unregister() + + if keycode or modifiers: + logger.debug('Creating thread worker...') + self.thread = threading.Thread( + target=self.worker, + name=f'Hotkey "{keycode}:{modifiers}"', + args=(keycode, modifiers) + ) + self.thread.daemon = True + logger.debug('Starting thread worker...') + self.thread.start() + logger.debug('Done.') + + def unregister(self) -> None: + """Unregister the hotkey handling.""" + thread = self.thread + + if thread: + logger.debug('Thread is/was running') + self.thread = None # type: ignore + logger.debug('Telling thread WM_QUIT') + PostThreadMessage(thread.ident, WM_QUIT, 0, 0) + logger.debug('Joining thread') + thread.join() # Wait for it to unregister hotkey and quit + + else: + logger.debug('No thread') + + logger.debug('Done.') + + def worker(self, keycode, modifiers) -> None: # noqa: CCR001 + """Handle hotkeys.""" + logger.debug('Begin...') + # Hotkey must be registered by the thread that handles it + if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): + logger.debug("We're not the right thread?") + self.thread = None # type: ignore + return + + fake = INPUT(INPUT_KEYBOARD, INPUTUNION(ki=KEYBDINPUT(keycode, keycode, 0, 0, None))) + + msg = MSG() + logger.debug('Entering GetMessage() loop...') + while GetMessage(ctypes.byref(msg), None, 0, 0) != 0: + logger.debug('Got message') + if msg.message == WM_HOTKEY: + logger.debug('WM_HOTKEY') + + if ( + config.get_int('hotkey_always') + or window_title(GetForegroundWindow()).startswith('Elite - Dangerous') + ): + if not config.shutting_down: + logger.debug('Sending event <>') + self.root.event_generate('<>', when="tail") + + else: + logger.debug('Passing key on') + UnregisterHotKey(None, 1) + SendInput(1, fake, ctypes.sizeof(INPUT)) + if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): + logger.debug("We aren't registered for this ?") + break + + elif msg.message == WM_SND_GOOD: + logger.debug('WM_SND_GOOD') + winsound.PlaySound(self.snd_good, winsound.SND_MEMORY) # synchronous + + elif msg.message == WM_SND_BAD: + logger.debug('WM_SND_BAD') + winsound.PlaySound(self.snd_bad, winsound.SND_MEMORY) # synchronous + + else: + logger.debug('Something else') + TranslateMessage(ctypes.byref(msg)) + DispatchMessage(ctypes.byref(msg)) + + logger.debug('Exited GetMessage() loop.') + UnregisterHotKey(None, 1) + self.thread = None # type: ignore + logger.debug('Done.') + + def acquire_start(self) -> None: + """Start acquiring hotkey state via polling.""" + pass + + def acquire_stop(self) -> None: + """Stop acquiring hotkey state.""" + pass + + def fromevent(self, event) -> Optional[Union[bool, Tuple]]: # noqa: CCR001 + """ + Return configuration (keycode, modifiers) or None=clear or False=retain previous. + + event.state is a pain - it shows the state of the modifiers *before* a modifier key was pressed. + event.state *does* differentiate between left and right Ctrl and Alt and between Return and Enter + by putting KF_EXTENDED in bit 18, but RegisterHotKey doesn't differentiate. + + :param event: tk event ? + :return: False to retain previous, None to not use, else (keycode, modifiers) + """ + modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ + | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ + | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ + | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ + | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) + keycode = event.keycode + + if keycode in [VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN]: + return (0, modifiers) + + if not modifiers: + if keycode == VK_ESCAPE: # Esc = retain previous + return False + + elif keycode in [VK_BACK, VK_DELETE, VK_CLEAR, VK_OEM_CLEAR]: # BkSp, Del, Clear = clear hotkey + return None + + elif keycode in [VK_RETURN, VK_SPACE, VK_OEM_MINUS] or ord('A') <= keycode <= ord( + 'Z'): # don't allow keys needed for typing in System Map + winsound.MessageBeep() + return None + + elif (keycode in [VK_NUMLOCK, VK_SCROLL, VK_PROCESSKEY] + or VK_CAPITAL <= keycode <= VK_MODECHANGE): # ignore unmodified mode switch keys + return (0, modifiers) + + # See if the keycode is usable and available + if RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode): + UnregisterHotKey(None, 2) + return (keycode, modifiers) + + else: + winsound.MessageBeep() + return None + + def display(self, keycode, modifiers) -> str: + """ + Return displayable form of given hotkey + modifiers. + + :param keycode: + :param modifiers: + :return: string form + """ + text = '' + if modifiers & MOD_WIN: + text += u'❖+' + + if modifiers & MOD_CONTROL: + text += u'Ctrl+' + + if modifiers & MOD_ALT: + text += u'Alt+' + + if modifiers & MOD_SHIFT: + text += u'⇧+' + + if VK_NUMPAD0 <= keycode <= VK_DIVIDE: + text += u'№' + + if not keycode: + pass + + elif VK_F1 <= keycode <= VK_F24: + text += f'F{keycode + 1 - VK_F1}' + + elif keycode in WindowsHotkeyMgr.DISPLAY: # specials + text += WindowsHotkeyMgr.DISPLAY[keycode] + + else: + c = MapVirtualKey(keycode, 2) # printable ? + if not c: # oops not printable + text += u'⁈' + + elif c < 0x20: # control keys + text += chr(c + 0x40) + + else: + text += chr(c).upper() + + return text + + def play_good(self) -> None: + """Play the 'good' sound.""" + if self.thread: + PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) + + def play_bad(self) -> None: + """Play the 'bad' sound.""" + if self.thread: + PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) From a08eef32440378b6e50835f34f701d4649af8b88 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:24:01 +0000 Subject: [PATCH 17/72] flake8, hotkey/darwin: Just ignore this file for now It'll take digging into macOS-specific documents to type things sufficiently to pass the flake8 TAE001 "too few type annotations" check. --- .flake8 | 1 + hotkey/darwin.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index c8dc0cc6..7207bf81 100644 --- a/.flake8 +++ b/.flake8 @@ -8,6 +8,7 @@ exclude = venv/ .venv/ wix/ + hotkey/darwin.py # FIXME: Check under macOS VM at some point # Show exactly where in a line the error happened #show-source = True diff --git a/hotkey/darwin.py b/hotkey/darwin.py index 9989b4f9..102a73fa 100644 --- a/hotkey/darwin.py +++ b/hotkey/darwin.py @@ -56,7 +56,7 @@ class MacHotkeyMgr(AbstractHotkeyMgr): pathlib.Path(config.respath_path) / 'snd_bad.wav', False ) - def register(self, root: tk.Tk, keycode, modifiers) -> None: + def register(self, root: tk.Tk, keycode: int, modifiers: int) -> None: """ Register current hotkey for monitoring. From 85fc308d427b116cdb31546ec44ebe902d608a5c Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:29:53 +0000 Subject: [PATCH 18/72] outfitting.py: Correct indexing into two maps The current data literally only uses strings as indexes. --- outfitting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/outfitting.py b/outfitting.py index e59aa8d0..174e24b8 100644 --- a/outfitting.py +++ b/outfitting.py @@ -93,13 +93,13 @@ def lookup(module, ship_map, entitled=False) -> Optional[dict]: # noqa: C901, C # Countermeasures - e.g. Hpt_PlasmaPointDefence_Turret_Tiny elif name[0] == 'hpt' and name[1] in countermeasure_map: new['category'] = 'utility' - new['name'], new['rating'] = countermeasure_map[len(name) > 4 and (name[1], name[4]) or name[1]] + new['name'], new['rating'] = countermeasure_map[name[1]] new['class'] = weaponclass_map[name[-1]] # Utility - e.g. Hpt_CargoScanner_Size0_Class1 elif name[0] == 'hpt' and name[1] in utility_map: new['category'] = 'utility' - new['name'] = utility_map[len(name) > 4 and (name[1], name[4]) or name[1]] + new['name'] = utility_map[name[1]] if not name[2].startswith('size') or not name[3].startswith('class'): raise AssertionError(f'{module["id"]}: Unknown class/rating "{name[2]}/{name[3]}"') From 36bd08d7150d5320aa3983e7175f550b72b3238e Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:31:24 +0000 Subject: [PATCH 19/72] l10n.py: Two minor typing fixes --- l10n.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/l10n.py b/l10n.py index 83600383..e2c1143f 100755 --- a/l10n.py +++ b/l10n.py @@ -83,7 +83,7 @@ class _Translations: self.translations = {None: {}} builtins.__dict__['_'] = lambda x: str(x).replace(r'\"', '"').replace('{CR}', '\n') - def install(self, lang: str = None) -> None: # noqa: CCR001 + def install(self, lang: str | None = None) -> None: # noqa: CCR001 """ Install the translation function to the _ builtin. @@ -250,7 +250,7 @@ class _Locale: self.float_formatter.setMinimumFractionDigits_(5) self.float_formatter.setMaximumFractionDigits_(5) - def stringFromNumber(self, number: Union[float, int], decimals: int = None) -> str: # noqa: N802 + def stringFromNumber(self, number: Union[float, int], decimals: int | None = None) -> str: # noqa: N802 warnings.warn(DeprecationWarning('use _Locale.string_from_number instead.')) return self.string_from_number(number, decimals) # type: ignore From 777b38e179e21265f7d530a0aa4f0b68c8bb12d8 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:32:52 +0000 Subject: [PATCH 20/72] theme.py: Minor typing fix --- theme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme.py b/theme.py index e9ace26f..45886f16 100644 --- a/theme.py +++ b/theme.py @@ -131,7 +131,7 @@ class _Theme(object): THEME_TRANSPARENT = 2 def __init__(self) -> None: - self.active = None # Starts out with no theme + self.active: int | None = None # Starts out with no theme self.minwidth: Optional[int] = None self.widgets: Dict[tk.Widget | tk.BitmapImage, Set] = {} self.widgets_pair: List = [] From 4041890f391d9bcdc5ec9d3c81e7d445f85879a8 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:39:59 +0000 Subject: [PATCH 21/72] tests/config/_old_config.py: Minor typing fixes. --- tests/config/_old_config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/config/_old_config.py b/tests/config/_old_config.py index b0952903..0226bc36 100644 --- a/tests/config/_old_config.py +++ b/tests/config/_old_config.py @@ -138,7 +138,7 @@ class OldConfig(): self.identifier = f'uk.org.marginal.{appname.lower()}' NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier - self.default_journal_dir = join( + self.default_journal_dir: str | None = join( NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], 'Frontier Developments', 'Elite Dangerous' @@ -222,13 +222,13 @@ class OldConfig(): journaldir = known_folder_path(FOLDERID_SavedGames) if journaldir: - self.default_journal_dir = join(journaldir, 'Frontier Developments', 'Elite Dangerous') + self.default_journal_dir: str | None = join(journaldir, 'Frontier Developments', 'Elite Dangerous') else: self.default_journal_dir = None self.identifier = applongname - self.hkey = HKEY() + self.hkey: ctypes.c_void_p | None = HKEY() disposition = DWORD() if RegCreateKeyEx( HKEY_CURRENT_USER, @@ -376,7 +376,7 @@ class OldConfig(): mkdir(self.plugin_dir) self.internal_plugin_dir = join(dirname(__file__), 'plugins') - self.default_journal_dir = None + self.default_journal_dir: str | None = None self.home = expanduser('~') self.respath = dirname(__file__) self.identifier = f'uk.org.marginal.{appname.lower()}' From 872ab1b814630145d9d5952d5408180d1fe93892 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:43:51 +0000 Subject: [PATCH 22/72] hotkey/darwin: No need to sys.platform gate within this And it gets rid of a `pre-commit run --all-files mypy` error. --- hotkey/darwin.py | 175 +++++++++++++++++++++++------------------------ 1 file changed, 87 insertions(+), 88 deletions(-) diff --git a/hotkey/darwin.py b/hotkey/darwin.py index 102a73fa..a053a55f 100644 --- a/hotkey/darwin.py +++ b/hotkey/darwin.py @@ -153,117 +153,116 @@ class MacHotkeyMgr(AbstractHotkeyMgr): self.keycode = 0 self.modifiers = 0 - if sys.platform == 'darwin': # noqa: C901 - @objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_) - def _handler(self, event) -> None: - # use event.charactersIgnoringModifiers to handle composing characters like Alt-e - if ( - (event.modifierFlags() & self.MODIFIERMASK) == self.modifiers - and ord(event.charactersIgnoringModifiers()[0]) == self.keycode - ): - if config.get_int('hotkey_always'): + @objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_) + def _handler(self, event) -> None: + # use event.charactersIgnoringModifiers to handle composing characters like Alt-e + if ( + (event.modifierFlags() & self.MODIFIERMASK) == self.modifiers + and ord(event.charactersIgnoringModifiers()[0]) == self.keycode + ): + if config.get_int('hotkey_always'): + self.activated = True + + else: # Only trigger if game client is front process + front = NSWorkspace.sharedWorkspace().frontmostApplication() + if front and front.bundleIdentifier() == 'uk.co.frontier.EliteDangerous': self.activated = True - else: # Only trigger if game client is front process - front = NSWorkspace.sharedWorkspace().frontmostApplication() - if front and front.bundleIdentifier() == 'uk.co.frontier.EliteDangerous': - self.activated = True + def acquire_start(self) -> None: + """Start acquiring hotkey state via polling.""" + self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE + self.root.after_idle(self._acquire_poll) - def acquire_start(self) -> None: - """Start acquiring hotkey state via polling.""" - self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE - self.root.after_idle(self._acquire_poll) + def acquire_stop(self) -> None: + """Stop acquiring hotkey state.""" + self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - def acquire_stop(self) -> None: - """Stop acquiring hotkey state.""" - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE + def _acquire_poll(self) -> None: + """Perform a poll of current hotkey state.""" + if config.shutting_down: + return - def _acquire_poll(self) -> None: - """Perform a poll of current hotkey state.""" - if config.shutting_down: - return + # No way of signalling to Tkinter from within the monkey-patched event handler that doesn't + # cause Python to crash, so poll. + if self.acquire_state: + if self.acquire_state == MacHotkeyMgr.ACQUIRE_NEW: + # Abuse tkEvent's keycode field to hold our acquired key & modifier + self.root.event_generate('', keycode=self.acquire_key) + self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE + self.root.after(50, self._acquire_poll) - # No way of signalling to Tkinter from within the monkey-patched event handler that doesn't - # cause Python to crash, so poll. - if self.acquire_state: - if self.acquire_state == MacHotkeyMgr.ACQUIRE_NEW: - # Abuse tkEvent's keycode field to hold our acquired key & modifier - self.root.event_generate('', keycode=self.acquire_key) - self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE - self.root.after(50, self._acquire_poll) + def fromevent(self, event) -> Optional[Union[bool, Tuple]]: + """ + Return configuration (keycode, modifiers) or None=clear or False=retain previous. - def fromevent(self, event) -> Optional[Union[bool, Tuple]]: - """ - Return configuration (keycode, modifiers) or None=clear or False=retain previous. + :param event: tk event ? + :return: False to retain previous, None to not use, else (keycode, modifiers) + """ + (keycode, modifiers) = (event.keycode & 0xffff, event.keycode & 0xffff0000) # Set by _acquire_poll() + if ( + keycode + and not (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask)) + ): + if keycode == 0x1b: # Esc = retain previous + self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE + return False - :param event: tk event ? - :return: False to retain previous, None to not use, else (keycode, modifiers) - """ - (keycode, modifiers) = (event.keycode & 0xffff, event.keycode & 0xffff0000) # Set by _acquire_poll() - if ( - keycode - and not (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask)) - ): - if keycode == 0x1b: # Esc = retain previous - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - return False + # BkSp, Del, Clear = clear hotkey + elif keycode in [0x7f, ord(NSDeleteFunctionKey), ord(NSClearLineFunctionKey)]: + self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE + return None - # BkSp, Del, Clear = clear hotkey - elif keycode in [0x7f, ord(NSDeleteFunctionKey), ord(NSClearLineFunctionKey)]: - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - return None + # don't allow keys needed for typing in System Map + elif keycode in [0x13, 0x20, 0x2d] or 0x61 <= keycode <= 0x7a: + NSBeep() + self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE + return None - # don't allow keys needed for typing in System Map - elif keycode in [0x13, 0x20, 0x2d] or 0x61 <= keycode <= 0x7a: - NSBeep() - self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE - return None + return (keycode, modifiers) - return (keycode, modifiers) + def display(self, keycode, modifiers) -> str: + """ + Return displayable form of given hotkey + modifiers. - def display(self, keycode, modifiers) -> str: - """ - Return displayable form of given hotkey + modifiers. + :param keycode: + :param modifiers: + :return: string form + """ + text = '' + if modifiers & NSControlKeyMask: + text += u'⌃' - :param keycode: - :param modifiers: - :return: string form - """ - text = '' - if modifiers & NSControlKeyMask: - text += u'⌃' + if modifiers & NSAlternateKeyMask: + text += u'⌥' - if modifiers & NSAlternateKeyMask: - text += u'⌥' + if modifiers & NSShiftKeyMask: + text += u'⇧' - if modifiers & NSShiftKeyMask: - text += u'⇧' + if modifiers & NSCommandKeyMask: + text += u'⌘' - if modifiers & NSCommandKeyMask: - text += u'⌘' + if (modifiers & NSNumericPadKeyMask) and keycode <= 0x7f: + text += u'№' - if (modifiers & NSNumericPadKeyMask) and keycode <= 0x7f: - text += u'№' + if not keycode: + pass - if not keycode: - pass + elif ord(NSF1FunctionKey) <= keycode <= ord(NSF35FunctionKey): + text += f'F{keycode + 1 - ord(NSF1FunctionKey)}' - elif ord(NSF1FunctionKey) <= keycode <= ord(NSF35FunctionKey): - text += f'F{keycode + 1 - ord(NSF1FunctionKey)}' + elif keycode in MacHotkeyMgr.DISPLAY: # specials + text += MacHotkeyMgr.DISPLAY[keycode] - elif keycode in MacHotkeyMgr.DISPLAY: # specials - text += MacHotkeyMgr.DISPLAY[keycode] + elif keycode < 0x20: # control keys + text += chr(keycode + 0x40) - elif keycode < 0x20: # control keys - text += chr(keycode + 0x40) + elif keycode < 0xf700: # key char + text += chr(keycode).upper() - elif keycode < 0xf700: # key char - text += chr(keycode).upper() + else: + text += u'⁈' - else: - text += u'⁈' - - return text + return text def play_good(self): """Play the 'good' sound.""" From f52ffce79fa49575c4d467d4ef37d4cd25764c93 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:45:37 +0000 Subject: [PATCH 23/72] timeout_session: Minor typing fix --- timeout_session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/timeout_session.py b/timeout_session.py index 6faf54bb..8a4656ce 100644 --- a/timeout_session.py +++ b/timeout_session.py @@ -25,7 +25,9 @@ class TimeoutAdapter(HTTPAdapter): return super().send(*args, **kwargs) -def new_session(timeout: int = REQUEST_TIMEOUT, session: requests.Session = None) -> requests.Session: +def new_session( + timeout: int = REQUEST_TIMEOUT, session: requests.Session | None = None +) -> requests.Session: """ Create a new requests.Session and override the default HTTPAdapter with a TimeoutAdapter. From 25dfb0588a978a3bcbdcea1741ee3a9f82ced1f4 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:50:15 +0000 Subject: [PATCH 24/72] tests/journal_lock: Minor typing fixes. --- journal_lock.py | 2 +- tests/journal_lock.py/test_journal_lock.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/journal_lock.py b/journal_lock.py index b8882a51..7945ef91 100644 --- a/journal_lock.py +++ b/journal_lock.py @@ -33,7 +33,7 @@ class JournalLock: def __init__(self) -> None: """Initialise where the journal directory and lock file are.""" - self.journal_dir: str = config.get_str('journaldir') or config.default_journal_dir + self.journal_dir: str | None = config.get_str('journaldir') or config.default_journal_dir self.journal_dir_path: Optional[pathlib.Path] = None self.set_path_from_journaldir() self.journal_dir_lockfile_name: Optional[pathlib.Path] = None diff --git a/tests/journal_lock.py/test_journal_lock.py b/tests/journal_lock.py/test_journal_lock.py index 72252e9f..7bfd4ea8 100644 --- a/tests/journal_lock.py/test_journal_lock.py +++ b/tests/journal_lock.py/test_journal_lock.py @@ -121,7 +121,7 @@ class TestJournalLock: tmp_path_factory: _pytest_tmpdir.TempPathFactory ) -> _pytest_tmpdir.TempPathFactory: """Fixture for mocking config.get_str('journaldir').""" - def get_str(key: str, *, default: str = None) -> str: + def get_str(key: str, *, default: str | None = None) -> str: """Mock config.*Config get_str to provide fake journaldir.""" if key == 'journaldir': return str(tmp_path_factory.getbasetemp()) @@ -140,7 +140,7 @@ class TestJournalLock: tmp_path_factory: _pytest_tmpdir.TempPathFactory ) -> _pytest_tmpdir.TempPathFactory: """Fixture for mocking config.get_str('journaldir').""" - def get_str(key: str, *, default: str = None) -> str: + def get_str(key: str, *, default: str | None = None) -> str: """Mock config.*Config get_str to provide fake journaldir.""" if key == 'journaldir': return tmp_path_factory.mktemp("changing") From 9377bbf225e239eae156f08c238696f05d0423c0 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 13:58:23 +0000 Subject: [PATCH 25/72] edshipyard: Minor typing fix --- edshipyard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edshipyard.py b/edshipyard.py index d709a2e1..1bfa77e6 100644 --- a/edshipyard.py +++ b/edshipyard.py @@ -81,7 +81,7 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001 if not v: continue - module: __Module = outfitting.lookup(v['module'], ship_map) + module: __Module | None = outfitting.lookup(v['module'], ship_map) if not module: continue From 7fe86c586212e12691aeef163cbb03ba27cafbe4 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 14:00:31 +0000 Subject: [PATCH 26/72] companion.py: Minor typing fixes --- companion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/companion.py b/companion.py index 41fc3b7d..c89e6319 100644 --- a/companion.py +++ b/companion.py @@ -326,7 +326,7 @@ class Auth(object): """ logger.debug(f'Trying for "{self.cmdr}"') - should_return, new_data = killswitch.check_killswitch('capi.auth', {}) + should_return, _ = killswitch.check_killswitch('capi.auth', {}) if should_return: logger.warning('capi.auth has been disabled via killswitch. Returning.') return None @@ -663,7 +663,7 @@ class Session(object): :return: True if login succeeded, False if re-authorization initiated. """ - should_return, new_data = killswitch.check_killswitch('capi.auth', {}) + should_return, _ = killswitch.check_killswitch('capi.auth', {}) if should_return: logger.warning('capi.auth has been disabled via killswitch. Returning.') return False @@ -781,7 +781,7 @@ class Session(object): :return: The resulting CAPI data, of type CAPIData. """ capi_data: CAPIData = CAPIData() - should_return, new_data = killswitch.check_killswitch('capi.request.' + capi_endpoint, {}) + should_return, _ = killswitch.check_killswitch('capi.request.' + capi_endpoint, {}) if should_return: logger.warning(f"capi.request.{capi_endpoint} has been disabled by killswitch. Returning.") return capi_data From 95b442cf028e90a9b9e2fc46ebf8ae0e88ee4b26 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 14:04:34 +0000 Subject: [PATCH 27/72] stats.py: Minor typing fixes. --- stats.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/stats.py b/stats.py index 6886eb23..f7f15b86 100644 --- a/stats.py +++ b/stats.py @@ -442,7 +442,9 @@ class StatsResults(tk.Toplevel): ): self.geometry(f"+{position.left}+{position.top}") - def addpage(self, parent, header: List[str] = None, align: Optional[str] = None) -> tk.Frame: + def addpage( + self, parent, header: List[str] | None = None, align: str | None = None + ) -> ttk.Frame: """ Add a page to the StatsResults screen. @@ -462,7 +464,7 @@ class StatsResults(tk.Toplevel): return page - def addpageheader(self, parent: tk.Frame, header: Sequence[str], align: Optional[str] = None) -> None: + def addpageheader(self, parent: ttk.Frame, header: Sequence[str], align: Optional[str] = None) -> None: """ Add the column headers to the page, followed by a separator. @@ -478,7 +480,7 @@ class StatsResults(tk.Toplevel): self.addpagerow(parent, ['']) def addpagerow( - self, parent: tk.Frame, content: Sequence[str], align: Optional[str] = None, with_copy: bool = False + self, parent: ttk.Frame, content: Sequence[str], align: Optional[str] = None, with_copy: bool = False ): """ Add a single row to parent. From fb065c5b788fd865196cc2c00eff6fff67bf25e8 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 14:06:40 +0000 Subject: [PATCH 28/72] collate.py: file paths/names should be pathlib.Path --- collate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collate.py b/collate.py index 3ab5224b..48007527 100755 --- a/collate.py +++ b/collate.py @@ -100,7 +100,7 @@ def addmodules(data): # noqa: C901, CCR001 if not data['lastStarport'].get('modules'): return - outfile = 'outfitting.csv' + outfile = pathlib.Path('outfitting.csv') modules = {} fields = ('id', 'symbol', 'category', 'name', 'mount', 'guidance', 'ship', 'class', 'rating', 'entitlement') From 26b12f5b14a6ef14287141047424d58b3bcebcec Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 14:10:25 +0000 Subject: [PATCH 29/72] plugins/inara, plug.py: Minor typing fixes --- plug.py | 2 +- plugins/inara.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plug.py b/plug.py index deeb6ebe..01f2fc2c 100644 --- a/plug.py +++ b/plug.py @@ -198,7 +198,7 @@ def provides(fn_name: str) -> List[str]: def invoke( - plugin_name: str, fallback: str, fn_name: str, *args: Tuple + plugin_name: str, fallback: str | None, fn_name: str, *args: Tuple ) -> Optional[str]: """ Invoke a function on a named plugin. diff --git a/plugins/inara.py b/plugins/inara.py index f3f32741..3501727c 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -35,6 +35,7 @@ from typing import OrderedDict as OrderedDictT from typing import Sequence, Union, cast import requests +import ttk import edmc_data import killswitch @@ -130,7 +131,7 @@ class This: self.log_button: nb.Checkbutton self.label: HyperlinkLabel self.apikey: nb.Entry - self.apikey_label: HyperlinkLabel + self.apikey_label: tk.Label self.events: Dict[Credentials, Deque[Event]] = defaultdict(deque) self.event_lock: Lock = threading.Lock() # protects events, for use when rewriting events @@ -235,7 +236,7 @@ def plugin_stop() -> None: logger.debug('Done.') -def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: +def plugin_prefs(parent: ttk.Frame, cmdr: str, is_beta: bool) -> tk.Frame: """Plugin Preferences UI hook.""" x_padding = 10 x_button_padding = 12 # indent Checkbuttons and Radiobuttons From 9e605d31c73d40b71750f93e6dcaf5237a944e5e Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 14:22:06 +0000 Subject: [PATCH 30/72] EDMC.py: Fix up other call to companion.session.station() * Opening of latest journal file didn't match how done in monitor.py. This caused `str` instead of `bytes` being passed to `monitor.parse_entry()`. * It was assuming pre-threaded return of data. Now properly gets it from the queue. --- EDMC.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/EDMC.py b/EDMC.py index 4daf4af3..2a7d7cd4 100755 --- a/EDMC.py +++ b/EDMC.py @@ -71,7 +71,7 @@ def versioncmp(versionstring) -> List: return list(map(int, versionstring.split('.'))) -def deep_get(target: dict, *args: str, default=None) -> Any: +def deep_get(target: dict | companion.CAPIData, *args: str, default=None) -> Any: """ Walk into a dict and return the specified deep value. @@ -224,12 +224,15 @@ sys.path: {sys.path}''' logger.debug(f'logdir = "{monitor.currentdir}"') logfile = monitor.journal_newest_filename(monitor.currentdir) + if logfile is None: + raise ValueError("None from monitor.journal_newest_filename") logger.debug(f'Using logfile "{logfile}"') - with open(logfile, 'r', encoding='utf-8') as loghandle: + with open(logfile, 'rb', 0) as loghandle: for line in loghandle: try: monitor.parse_entry(line) + except Exception: logger.debug(f'Invalid journal entry {line!r}') @@ -410,7 +413,23 @@ sys.path: {sys.path}''' # Retry for shipyard sleep(SERVER_RETRY) - new_data = companion.session.station() + companion.session.station(int(time())) + # Wait for the response + _capi_request_timeout = 60 + try: + capi_response = companion.session.capi_response_queue.get( + block=True, timeout=_capi_request_timeout + ) + + except queue.Empty: + logger.error(f'CAPI requests timed out after {_capi_request_timeout} seconds') + sys.exit(EXIT_SERVER) + + if isinstance(capi_response, companion.EDMCCAPIFailedRequest): + logger.error(f'Failed Request: {capi_response.message}') + sys.exit(EXIT_SERVER) + + new_data = capi_response.capi_data # might have undocked while we were waiting for retry in which case station data is unreliable if new_data['commander'].get('docked') and \ deep_get(new_data, 'lastSystem', 'name') == monitor.system and \ From 3b0c7370cc8804bb8d0dde544c7e1243b262373a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 14:43:51 +0000 Subject: [PATCH 31/72] hotkey: Make more of the windows methods abstract ones in parent Without this mypy objects to calling them due to not being in the visible AbstractKetkeyMgr type. --- hotkey/__init__.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/hotkey/__init__.py b/hotkey/__init__.py index 782a16fa..5162621c 100644 --- a/hotkey/__init__.py +++ b/hotkey/__init__.py @@ -4,6 +4,7 @@ import abc import sys from abc import abstractmethod +from typing import Optional, Tuple, Union class AbstractHotkeyMgr(abc.ABC): @@ -19,6 +20,41 @@ class AbstractHotkeyMgr(abc.ABC): """Unregister the hotkey handling.""" pass + @abstractmethod + def acquire_start(self) -> None: + """Start acquiring hotkey state via polling.""" + pass + + @abstractmethod + def acquire_stop(self) -> None: + """Stop acquiring hotkey state.""" + pass + + @abstractmethod + def fromevent(self, event) -> Optional[Union[bool, Tuple]]: + """ + Return configuration (keycode, modifiers) or None=clear or False=retain previous. + + event.state is a pain - it shows the state of the modifiers *before* a modifier key was pressed. + event.state *does* differentiate between left and right Ctrl and Alt and between Return and Enter + by putting KF_EXTENDED in bit 18, but RegisterHotKey doesn't differentiate. + + :param event: tk event ? + :return: False to retain previous, None to not use, else (keycode, modifiers) + """ + pass + + @abstractmethod + def display(self, keycode: int, modifiers: int) -> str: + """ + Return displayable form of given hotkey + modifiers. + + :param keycode: + :param modifiers: + :return: string form + """ + pass + @abstractmethod def play_good(self) -> None: """Play the 'good' sound.""" From f4b150960cbd4b8e5a748a167b90b1fc116e8dfe Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 14:50:05 +0000 Subject: [PATCH 32/72] prefs.py & related files: Fix up mypy type checking * Some trivial. * As myNotebook.py's class is based on `ttk.Notebook`, typing changed to that. --- EDMCLogging.py | 4 ++-- myNotebook.py | 2 +- plug.py | 8 +++++--- prefs.py | 19 +++++++++---------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/EDMCLogging.py b/EDMCLogging.py index 8bde0760..e698b35b 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -216,7 +216,7 @@ class Logger: """ return self.logger_channel - def set_channels_loglevel(self, level: int) -> None: + def set_channels_loglevel(self, level: int | str) -> None: """ Set the specified log level on the channels. @@ -226,7 +226,7 @@ class Logger: self.logger_channel.setLevel(level) self.logger_channel_rotating.setLevel(level) - def set_console_loglevel(self, level: int) -> None: + def set_console_loglevel(self, level: int | str) -> None: """ Set the specified log level on the console channel. diff --git a/myNotebook.py b/myNotebook.py index eb17bf2f..1989141f 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -58,7 +58,7 @@ class Notebook(ttk.Notebook): class Frame(sys.platform == 'darwin' and tk.Frame or ttk.Frame): # type: ignore """Custom t(t)k.Frame class to fix some display issues.""" - def __init__(self, master: Optional[ttk.Frame] = None, **kw): + def __init__(self, master: ttk.Notebook | None = None, **kw): if sys.platform == 'darwin': kw['background'] = kw.pop('background', PAGEBG) tk.Frame.__init__(self, master, **kw) diff --git a/plug.py b/plug.py index 01f2fc2c..68a2d26b 100644 --- a/plug.py +++ b/plug.py @@ -9,6 +9,8 @@ import tkinter as tk from builtins import object, str from typing import Any, Callable, List, Mapping, MutableMapping, Optional, Tuple +import ttk + import companion import myNotebook as nb # noqa: N813 from config import config @@ -118,7 +120,7 @@ class Plugin(object): return None - def get_prefs(self, parent: tk.Frame, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: + def get_prefs(self, parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> Optional[tk.Frame]: """ If the plugin provides a prefs frame, create and return it. @@ -248,7 +250,7 @@ def notify_stop() -> Optional[str]: return error -def notify_prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: +def notify_prefs_cmdr_changed(cmdr: str | None, is_beta: bool) -> None: """ Notify plugins that the Cmdr was changed while the settings dialog is open. @@ -265,7 +267,7 @@ def notify_prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: logger.exception(f'Plugin "{plugin.name}" failed') -def notify_prefs_changed(cmdr: str, is_beta: bool) -> None: +def notify_prefs_changed(cmdr: str | None, is_beta: bool) -> None: """ Notify plugins that the settings dialog has been closed. diff --git a/prefs.py b/prefs.py index 985ef581..e5ab242b 100644 --- a/prefs.py +++ b/prefs.py @@ -19,7 +19,6 @@ from EDMCLogging import edmclogger, get_main_logger from hotkey import hotkeymgr from l10n import Translations from monitor import monitor -from myNotebook import Notebook from theme import theme from ttkHyperlinkLabel import HyperlinkLabel @@ -279,7 +278,7 @@ class PreferencesDialog(tk.Toplevel): frame = ttk.Frame(self) frame.grid(sticky=tk.NSEW) - notebook = nb.Notebook(frame) + notebook: ttk.Notebook = nb.Notebook(frame) notebook.bind('<>', self.tabchanged) # Recompute on tab change self.PADX = 10 @@ -333,7 +332,7 @@ class PreferencesDialog(tk.Toplevel): ): self.geometry(f"+{position.left}+{position.top}") - def __setup_output_tab(self, root_notebook: nb.Notebook) -> None: + def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None: output_frame = nb.Frame(root_notebook) output_frame.columnconfigure(0, weight=1) @@ -418,13 +417,13 @@ class PreferencesDialog(tk.Toplevel): # LANG: Label for 'Output' Settings/Preferences tab root_notebook.add(output_frame, text=_('Output')) # Tab heading in settings - def __setup_plugin_tabs(self, notebook: Notebook) -> None: + def __setup_plugin_tabs(self, notebook: ttk.Notebook) -> None: for plugin in plug.PLUGINS: plugin_frame = plugin.get_prefs(notebook, monitor.cmdr, monitor.is_beta) if plugin_frame: notebook.add(plugin_frame, text=plugin.name) - def __setup_config_tab(self, notebook: Notebook) -> None: # noqa: CCR001 + def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 config_frame = nb.Frame(notebook) config_frame.columnconfigure(1, weight=1) row = AutoInc(start=1) @@ -675,7 +674,7 @@ class PreferencesDialog(tk.Toplevel): # LANG: Label for 'Configuration' tab in Settings notebook.add(config_frame, text=_('Configuration')) - def __setup_privacy_tab(self, notebook: Notebook) -> None: + def __setup_privacy_tab(self, notebook: ttk.Notebook) -> None: frame = nb.Frame(notebook) self.hide_multicrew_captain = tk.BooleanVar(value=config.get_bool('hide_multicrew_captain', default=False)) self.hide_private_group = tk.BooleanVar(value=config.get_bool('hide_private_group', default=False)) @@ -697,7 +696,7 @@ class PreferencesDialog(tk.Toplevel): notebook.add(frame, text=_('Privacy')) # LANG: Preferences privacy tab title - def __setup_appearance_tab(self, notebook: Notebook) -> None: + def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: self.languages = Translations.available_names() # Appearance theme and language setting # LANG: The system default language choice in Settings > Appearance @@ -887,7 +886,7 @@ class PreferencesDialog(tk.Toplevel): # LANG: Label for Settings > Appearance tab notebook.add(appearance_frame, text=_('Appearance')) # Tab heading in settings - def __setup_plugin_tab(self, notebook: Notebook) -> None: # noqa: CCR001 + def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 # Plugin settings and info plugins_frame = nb.Frame(notebook) plugins_frame.columnconfigure(0, weight=1) @@ -1188,8 +1187,8 @@ class PreferencesDialog(tk.Toplevel): :return: "break" as a literal, to halt processing """ good = hotkeymgr.fromevent(event) - if good: - (hotkey_code, hotkey_mods) = good + if good and isinstance(good, tuple): + hotkey_code, hotkey_mods = good event.widget.delete(0, tk.END) event.widget.insert(0, hotkeymgr.display(hotkey_code, hotkey_mods)) if hotkey_code: From ca233a40a8a13203e90b8cf889b013d4e9d320ac Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 14:54:08 +0000 Subject: [PATCH 33/72] plugins/coriolis: Minor type fixes. --- plugins/coriolis.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/coriolis.py b/plugins/coriolis.py index fef683b9..29fddf26 100644 --- a/plugins/coriolis.py +++ b/plugins/coriolis.py @@ -26,6 +26,7 @@ import gzip import io import json import tkinter as tk +from tkinter import ttk from typing import TYPE_CHECKING, Union import myNotebook as nb # noqa: N813 # its not my fault. @@ -79,7 +80,7 @@ def plugin_start3(path: str) -> str: return 'Coriolis' -def plugin_prefs(parent: tk.Widget, cmdr: str, is_beta: bool) -> tk.Frame: +def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Frame: """Set up plugin preferences.""" PADX = 10 # noqa: N806 @@ -126,7 +127,7 @@ def plugin_prefs(parent: tk.Widget, cmdr: str, is_beta: bool) -> tk.Frame: return conf_frame -def prefs_changed(cmdr: str, is_beta: bool) -> None: +def prefs_changed(cmdr: str | None, is_beta: bool) -> None: """Update URLs.""" global normal_url, beta_url, override_mode normal_url = normal_textvar.get() From 71cbfb835854843cddf4922bad5d88d8181ae473 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 16:35:02 +0000 Subject: [PATCH 34/72] plugins/edsm: Lots of type fixing, inc. conditionals where None is possible --- plug.py | 7 +- plugins/edsm.py | 250 +++++++++++++++++++++++++++--------------------- 2 files changed, 146 insertions(+), 111 deletions(-) diff --git a/plug.py b/plug.py index 68a2d26b..e6c4cd34 100644 --- a/plug.py +++ b/plug.py @@ -7,9 +7,8 @@ import os import sys import tkinter as tk from builtins import object, str -from typing import Any, Callable, List, Mapping, MutableMapping, Optional, Tuple - -import ttk +from tkinter import ttk +from typing import Any, Callable, List, Mapping, MutableMapping, Optional import companion import myNotebook as nb # noqa: N813 @@ -200,7 +199,7 @@ def provides(fn_name: str) -> List[str]: def invoke( - plugin_name: str, fallback: str | None, fn_name: str, *args: Tuple + plugin_name: str, fallback: str | None, fn_name: str, *args: Any ) -> Optional[str]: """ Invoke a function on a named plugin. diff --git a/plugins/edsm.py b/plugins/edsm.py index 130a794d..635b3559 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -38,12 +38,14 @@ from datetime import datetime, timedelta, timezone from queue import Queue from threading import Thread from time import sleep -from typing import TYPE_CHECKING, Any, List, Literal, Mapping, MutableMapping, Optional, Set, Tuple, Union, cast +from tkinter import ttk +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Mapping, MutableMapping, Optional, Set, Tuple, Union, cast import requests import killswitch import monitor +import myNotebook import myNotebook as nb # noqa: N813 import plug from companion import CAPIData @@ -82,8 +84,8 @@ class This: self.session: requests.Session = requests.Session() self.session.headers['User-Agent'] = user_agent self.queue: Queue = Queue() # Items to be sent to EDSM by worker thread - self.discarded_events: Set[str] = [] # List discarded events from EDSM - self.lastlookup: requests.Response # Result of last system lookup + self.discarded_events: Set[str] = set() # List discarded events from EDSM + self.lastlookup: Dict[str, Any] # Result of last system lookup # Game state self.multicrew: bool = False # don't send captain's ship info to EDSM while on a crew @@ -91,13 +93,13 @@ class This: self.newgame: bool = False # starting up - batch initial burst of events self.newgame_docked: bool = False # starting up while docked self.navbeaconscan: int = 0 # batch up burst of Scan events after NavBeaconScan - self.system_link: tk.Widget = None - self.system: tk.Tk = None - self.system_address: Optional[int] = None # Frontier SystemAddress - self.system_population: Optional[int] = None - self.station_link: tk.Widget = None - self.station: Optional[str] = None - self.station_marketid: Optional[int] = None # Frontier MarketID + self.system_link: tk.Widget | None = None + self.system: tk.Tk | None = None + self.system_address: int | None = None # Frontier SystemAddress + self.system_population: int | None = None + self.station_link: tk.Widget | None = None + self.station: str | None = None + self.station_marketid: int | None = None # Frontier MarketID self.on_foot = False self._IMG_KNOWN = None @@ -107,19 +109,19 @@ class This: self.thread: Optional[threading.Thread] = None - self.log = None - self.log_button = None + self.log: tk.IntVar | None = None + self.log_button: ttk.Checkbutton | None = None - self.label = None + self.label: tk.Widget | None = None - self.cmdr_label = None - self.cmdr_text = None + self.cmdr_label: myNotebook.Label | None = None + self.cmdr_text: myNotebook.Label | None = None - self.user_label = None - self.user = None + self.user_label: myNotebook.Label | None = None + self.user: myNotebook.Entry | None = None - self.apikey_label = None - self.apikey = None + self.apikey_label: myNotebook.Label | None = None + self.apikey: myNotebook.Entry | None = None this = This() @@ -267,7 +269,7 @@ def plugin_stop() -> None: logger.debug('Done.') -def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: +def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Frame: """ Plugin preferences setup hook. @@ -300,7 +302,8 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: frame, text=_('Send flight log and Cmdr status to EDSM'), variable=this.log, command=prefsvarchanged ) - this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5, 0), sticky=tk.W) + if this.log_button: + this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5, 0), sticky=tk.W) nb.Label(frame).grid(sticky=tk.W) # big spacer # Section heading in settings @@ -315,61 +318,77 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: cur_row = 10 - this.label.grid(columnspan=2, padx=PADX, sticky=tk.W) + if this.label: + this.label.grid(columnspan=2, padx=PADX, sticky=tk.W) - # LANG: Game Commander name label in EDSM settings - this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window - this.cmdr_label.grid(row=cur_row, padx=PADX, sticky=tk.W) - this.cmdr_text = nb.Label(frame) - this.cmdr_text.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.W) + if this.cmdr_label and this.cmdr_text: + # LANG: Game Commander name label in EDSM settings + this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window + this.cmdr_label.grid(row=cur_row, padx=PADX, sticky=tk.W) + this.cmdr_text = nb.Label(frame) + this.cmdr_text.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.W) cur_row += 1 - # LANG: EDSM Commander name label in EDSM settings - this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting - this.user_label.grid(row=cur_row, padx=PADX, sticky=tk.W) - this.user = nb.Entry(frame) - this.user.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.EW) + if this.user_label and this.label: + # LANG: EDSM Commander name label in EDSM settings + this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting + this.user_label.grid(row=cur_row, padx=PADX, sticky=tk.W) + this.user = nb.Entry(frame) + this.user.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.EW) cur_row += 1 - # LANG: EDSM API key label - this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting - this.apikey_label.grid(row=cur_row, padx=PADX, sticky=tk.W) - this.apikey = nb.Entry(frame) - this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.EW) + if this.apikey_label and this.apikey: + # LANG: EDSM API key label + this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting + this.apikey_label.grid(row=cur_row, padx=PADX, sticky=tk.W) + this.apikey = nb.Entry(frame) + this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.EW) prefs_cmdr_changed(cmdr, is_beta) return frame -def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: +def prefs_cmdr_changed(cmdr: str | None, is_beta: bool) -> None: # noqa: CCR001 """ Handle the Commander name changing whilst Settings was open. :param cmdr: The new current Commander name. :param is_beta: Whether game beta was detected. """ - this.log_button['state'] = tk.NORMAL if cmdr and not is_beta else tk.DISABLED - this.user['state'] = tk.NORMAL - this.user.delete(0, tk.END) - this.apikey['state'] = tk.NORMAL - this.apikey.delete(0, tk.END) + if this.log_button: + this.log_button['state'] = tk.NORMAL if cmdr and not is_beta else tk.DISABLED + + if this.user: + this.user['state'] = tk.NORMAL + this.user.delete(0, tk.END) + + if this.apikey: + this.apikey['state'] = tk.NORMAL + this.apikey.delete(0, tk.END) + if cmdr: - this.cmdr_text['text'] = f'{cmdr}{" [Beta]" if is_beta else ""}' + if this.cmdr_text: + this.cmdr_text['text'] = f'{cmdr}{" [Beta]" if is_beta else ""}' + cred = credentials(cmdr) if cred: - this.user.insert(0, cred[0]) - this.apikey.insert(0, cred[1]) + if this.user: + this.user.insert(0, cred[0]) + + if this.apikey: + this.apikey.insert(0, cred[1]) else: - # LANG: We have no data on the current commander - this.cmdr_text['text'] = _('None') + if this.cmdr_text: + # LANG: We have no data on the current commander + this.cmdr_text['text'] = _('None') to_set: Union[Literal['normal'], Literal['disabled']] = tk.DISABLED - if cmdr and not is_beta and this.log.get(): + if cmdr and not is_beta and this.log and this.log.get(): to_set = tk.NORMAL set_prefs_ui_states(to_set) @@ -378,7 +397,7 @@ def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: def prefsvarchanged() -> None: """Handle the 'Send data to EDSM' tickbox changing state.""" to_set = tk.DISABLED - if this.log.get(): + if this.log and this.log.get() and this.log_button: to_set = this.log_button['state'] set_prefs_ui_states(to_set) @@ -390,13 +409,17 @@ def set_prefs_ui_states(state: str) -> None: :param state: the state to set each entry to """ - this.label['state'] = state - this.cmdr_label['state'] = state - this.cmdr_text['state'] = state - this.user_label['state'] = state - this.user['state'] = state - this.apikey_label['state'] = state - this.apikey['state'] = state + if ( + this.label and this.cmdr_label and this.cmdr_text and this.user_label and this.user + and this.apikey_label and this.apikey + ): + this.label['state'] = state + this.cmdr_label['state'] = state + this.cmdr_text['state'] = state + this.user_label['state'] = state + this.user['state'] = state + this.apikey_label['state'] = state + this.apikey['state'] = state def prefs_changed(cmdr: str, is_beta: bool) -> None: @@ -406,7 +429,8 @@ def prefs_changed(cmdr: str, is_beta: bool) -> None: :param cmdr: Name of Commander. :param is_beta: Whether game beta was detected. """ - config.set('edsm_out', this.log.get()) + if this.log: + config.set('edsm_out', this.log.get()) if cmdr and not is_beta: # TODO: remove this when config is rewritten. @@ -414,17 +438,18 @@ def prefs_changed(cmdr: str, is_beta: bool) -> None: usernames: List[str] = config.get_list('edsm_usernames', default=[]) apikeys: List[str] = config.get_list('edsm_apikeys', default=[]) - if cmdr in cmdrs: - idx = cmdrs.index(cmdr) - usernames.extend([''] * (1 + idx - len(usernames))) - usernames[idx] = this.user.get().strip() - apikeys.extend([''] * (1 + idx - len(apikeys))) - apikeys[idx] = this.apikey.get().strip() + if this.user and this.apikey: + if cmdr in cmdrs: + idx = cmdrs.index(cmdr) + usernames.extend([''] * (1 + idx - len(usernames))) + usernames[idx] = this.user.get().strip() + apikeys.extend([''] * (1 + idx - len(apikeys))) + apikeys[idx] = this.apikey.get().strip() - else: - config.set('edsm_cmdrs', cmdrs + [cmdr]) - usernames.append(this.user.get().strip()) - apikeys.append(this.apikey.get().strip()) + else: + config.set('edsm_cmdrs', cmdrs + [cmdr]) + usernames.append(this.user.get().strip()) + apikeys.append(this.apikey.get().strip()) config.set('edsm_usernames', usernames) config.set('edsm_apikeys', apikeys) @@ -545,12 +570,13 @@ entry: {entry!r}''' else: to_set = '' - this.station_link['text'] = to_set - this.station_link['url'] = station_url(str(this.system), str(this.station)) - this.station_link.update_idletasks() + if this.station_link: + this.station_link['text'] = to_set + this.station_link['url'] = station_url(str(this.system), str(this.station)) + this.station_link.update_idletasks() # Update display of 'EDSM Status' image - if this.system_link['text'] != system: + if this.system_link and this.system_link['text'] != system: this.system_link['text'] = system if system else '' this.system_link['image'] = '' this.system_link.update_idletasks() @@ -639,7 +665,7 @@ Queueing: {entry!r}''' # Update system data -def cmdr_data(data: CAPIData, is_beta: bool) -> Optional[str]: +def cmdr_data(data: CAPIData, is_beta: bool) -> Optional[str]: # noqa: CCR001 """ Process new CAPI data. @@ -663,27 +689,29 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> Optional[str]: # TODO: Fire off the EDSM API call to trigger the callback for the icons if config.get_str('system_provider') == 'EDSM': - this.system_link['text'] = this.system - # Do *NOT* set 'url' here, as it's set to a function that will call - # through correctly. We don't want a static string. - this.system_link.update_idletasks() + if this.system_link: + this.system_link['text'] = this.system + # Do *NOT* set 'url' here, as it's set to a function that will call + # through correctly. We don't want a static string. + this.system_link.update_idletasks() if config.get_str('station_provider') == 'EDSM': - if data['commander']['docked'] or this.on_foot and this.station: - this.station_link['text'] = this.station + if this.station_link: + if data['commander']['docked'] or this.on_foot and this.station: + this.station_link['text'] = this.station - elif data['lastStarport']['name'] and data['lastStarport']['name'] != "": - this.station_link['text'] = STATION_UNDOCKED + elif data['lastStarport']['name'] and data['lastStarport']['name'] != "": + this.station_link['text'] = STATION_UNDOCKED - else: - this.station_link['text'] = '' + else: + this.station_link['text'] = '' - # Do *NOT* set 'url' here, as it's set to a function that will call - # through correctly. We don't want a static string. + # Do *NOT* set 'url' here, as it's set to a function that will call + # through correctly. We don't want a static string. - this.station_link.update_idletasks() + this.station_link.update_idletasks() - if not this.system_link['text']: + if this.system_link and not this.system_link['text']: this.system_link['text'] = system this.system_link['image'] = '' this.system_link.update_idletasks() @@ -759,9 +787,12 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently retrying = 0 while retrying < 3: + if item is None: + item = cast(Tuple[str, str, str, Mapping[str, Any]], ("", {})) + should_skip, new_item = killswitch.check_killswitch( 'plugins.edsm.worker', - item if item is not None else cast(Tuple[str, Mapping[str, Any]], ("", {})), + item, logger ) @@ -838,8 +869,12 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently if any(p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')): data_elided = data.copy() data_elided['apiKey'] = '' - data_elided['message'] = data_elided['message'].decode('utf-8') - data_elided['commanderName'] = data_elided['commanderName'].decode('utf-8') + if isinstance(data_elided['message'], bytes): + data_elided['message'] = data_elided['message'].decode('utf-8') + + if isinstance(data_elided['commanderName'], bytes): + data_elided['commanderName'] = data_elided['commanderName'].decode('utf-8') + logger.trace_if( 'journal.locations', "pending has at least one of ('CarrierJump', 'FSDJump', 'Location', 'Docked')" @@ -858,11 +893,11 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently 'journal.locations', f'Overall POST data (elided) is:\n{json.dumps(data_elided, indent=2)}' ) - r = this.session.post(TARGET_URL, data=data, timeout=_TIMEOUT) - logger.trace_if('plugin.edsm.api', f'API response content: {r.content!r}') - r.raise_for_status() + response = this.session.post(TARGET_URL, data=data, timeout=_TIMEOUT) + logger.trace_if('plugin.edsm.api', f'API response content: {response.content!r}') + response.raise_for_status() - reply = r.json() + reply = response.json() msg_num = reply['msgnum'] msg = reply['msg'] # 1xx = OK @@ -893,7 +928,7 @@ def worker() -> None: # noqa: CCR001 C901 # Cant be broken up currently # Update main window's system status this.lastlookup = r # calls update_status in main thread - if not config.shutting_down: + if not config.shutting_down and this.system_link is not None: this.system_link.event_generate('<>', when="tail") if r['msgnum'] // 100 != 1: # type: ignore @@ -996,18 +1031,19 @@ def update_status(event=None) -> None: # msgnum: 1xx = OK, 2xx = fatal error, 3xx = error, 4xx = ignorable errors. def edsm_notify_system(reply: Mapping[str, Any]) -> None: """Update the image next to the system link.""" - if not reply: - this.system_link['image'] = this._IMG_ERROR - # LANG: EDSM Plugin - Error connecting to EDSM API - plug.show_error(_("Error: Can't connect to EDSM")) + if this.system_link is not None: + if not reply: + this.system_link['image'] = this._IMG_ERROR + # LANG: EDSM Plugin - Error connecting to EDSM API + plug.show_error(_("Error: Can't connect to EDSM")) - elif reply['msgnum'] // 100 not in (1, 4): - this.system_link['image'] = this._IMG_ERROR - # LANG: EDSM Plugin - Error message from EDSM API - plug.show_error(_('Error: EDSM {MSG}').format(MSG=reply['msg'])) + elif reply['msgnum'] // 100 not in (1, 4): + this.system_link['image'] = this._IMG_ERROR + # LANG: EDSM Plugin - Error message from EDSM API + plug.show_error(_('Error: EDSM {MSG}').format(MSG=reply['msg'])) - elif reply.get('systemCreated'): - this.system_link['image'] = this._IMG_NEW + elif reply.get('systemCreated'): + this.system_link['image'] = this._IMG_NEW - else: - this.system_link['image'] = this._IMG_KNOWN + else: + this.system_link['image'] = this._IMG_KNOWN From e66bae090bc6f1a02f842cc106bce56279cebf24 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 16:35:42 +0000 Subject: [PATCH 35/72] plugins/inara: Minor typing fixes --- plugins/inara.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/inara.py b/plugins/inara.py index 3501727c..51b7e270 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -30,12 +30,12 @@ from dataclasses import dataclass from datetime import datetime, timedelta, timezone from operator import itemgetter from threading import Lock, Thread +from tkinter import ttk from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, List, Mapping, NamedTuple, Optional from typing import OrderedDict as OrderedDictT from typing import Sequence, Union, cast import requests -import ttk import edmc_data import killswitch @@ -236,7 +236,7 @@ def plugin_stop() -> None: logger.debug('Done.') -def plugin_prefs(parent: ttk.Frame, cmdr: str, is_beta: bool) -> tk.Frame: +def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> tk.Frame: """Plugin Preferences UI hook.""" x_padding = 10 x_button_padding = 12 # indent Checkbuttons and Radiobuttons From 2c11aef1be7cc1ef0269d429c17fd67b769d3a98 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 16:46:39 +0000 Subject: [PATCH 36/72] plugins/eddn: Use correct `logging` function & `new_data` typing * `logger.INFO` will, at best, be a constant, it should be `logger.info()`. * When we're not interested in the `new_data` 2nd part of the tuple from `killswitches.check_killswitch()` we can't use `_` as there's a potential class with the `l10n.py` injection of `_()` as a builtin. And you can't declare types withing first-use in a return-tuple. So, declare them on their own lines, with throwaway default values instead. --- plugins/eddn.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/eddn.py b/plugins/eddn.py index b1df07b7..33ee1802 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -379,7 +379,7 @@ class EDDNSender: :param text: The status text to be set/logged. """ if os.getenv('EDMC_NO_UI'): - logger.INFO(text) + logger.info(text) return self.eddn.parent.children['status']['text'] = text @@ -635,6 +635,8 @@ class EDDN: :param data: a dict containing the starport data :param is_beta: whether or not we're currently in beta mode """ + should_return: bool = False + new_data: Dict[str, Any] = {} should_return, new_data = killswitch.check_killswitch('capi.request./market', {}) if should_return: logger.warning("capi.request./market has been disabled by killswitch. Returning.") @@ -766,6 +768,8 @@ class EDDN: :param data: dict containing the outfitting data :param is_beta: whether or not we're currently in beta mode """ + should_return: bool = False + new_data: Dict[str, Any] = {} should_return, new_data = killswitch.check_killswitch('capi.request./shipyard', {}) if should_return: logger.warning("capi.request./shipyard has been disabled by killswitch. Returning.") @@ -832,6 +836,8 @@ class EDDN: :param data: dict containing the shipyard data :param is_beta: whether or not we are in beta mode """ + should_return: bool = False + new_data: Dict[str, Any] = {} should_return, new_data = killswitch.check_killswitch('capi.request./shipyard', {}) if should_return: logger.warning("capi.request./shipyard has been disabled by killswitch. Returning.") From ce4a6ff898138fdeb41896a8aa35db2e31505284 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 16:54:03 +0000 Subject: [PATCH 37/72] plugins/eddn: Remove the 'default values' from should_retrn & new_data In *this* case the variables *are* used in the scope so become bound, so we can get away with bare type declaration. --- plugins/eddn.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/eddn.py b/plugins/eddn.py index 33ee1802..3cd2d819 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -635,8 +635,8 @@ class EDDN: :param data: a dict containing the starport data :param is_beta: whether or not we're currently in beta mode """ - should_return: bool = False - new_data: Dict[str, Any] = {} + should_return: bool + new_data: Dict[str, Any] should_return, new_data = killswitch.check_killswitch('capi.request./market', {}) if should_return: logger.warning("capi.request./market has been disabled by killswitch. Returning.") @@ -768,8 +768,8 @@ class EDDN: :param data: dict containing the outfitting data :param is_beta: whether or not we're currently in beta mode """ - should_return: bool = False - new_data: Dict[str, Any] = {} + should_return: bool + new_data: Dict[str, Any] should_return, new_data = killswitch.check_killswitch('capi.request./shipyard', {}) if should_return: logger.warning("capi.request./shipyard has been disabled by killswitch. Returning.") @@ -836,8 +836,8 @@ class EDDN: :param data: dict containing the shipyard data :param is_beta: whether or not we are in beta mode """ - should_return: bool = False - new_data: Dict[str, Any] = {} + should_return: bool + new_data: Dict[str, Any] should_return, new_data = killswitch.check_killswitch('capi.request./shipyard', {}) if should_return: logger.warning("capi.request./shipyard has been disabled by killswitch. Returning.") From 37b054b3d36a0cad3bade6577e6ae77d759bd436 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 16:58:53 +0000 Subject: [PATCH 38/72] Fix type of `master` passed to `plug.load_plugins()` --- plug.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plug.py b/plug.py index e6c4cd34..8e19a6cf 100644 --- a/plug.py +++ b/plug.py @@ -27,7 +27,7 @@ class LastError: """Holds the last plugin error.""" msg: Optional[str] - root: tk.Frame + root: tk.Tk def __init__(self) -> None: self.msg = None @@ -141,7 +141,7 @@ class Plugin(object): return None -def load_plugins(master: tk.Frame) -> None: # noqa: CCR001 +def load_plugins(master: tk.Tk) -> None: # noqa: CCR001 """Find and load all plugins.""" last_error.root = master From e45a89d97016dceaea1c647f9fda634cee8ab693 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 17:03:07 +0000 Subject: [PATCH 39/72] EDMarketConnector.py: Minor type fixes & ttkHyperlink.py too --- EDMarketConnector.py | 10 +++++++--- ttkHyperlinkLabel.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index bed58e99..11f103b4 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -16,7 +16,7 @@ from builtins import object, str from os import chdir, environ from os.path import dirname, join from time import localtime, strftime, time -from typing import TYPE_CHECKING, Literal, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple, Union # Have this as early as possible for people running EDMarketConnector.exe # from cmd.exe or a bat file or similar. Else they might not be in the correct @@ -592,7 +592,7 @@ class AppWindow(object): # The type needs defining for adding the menu entry, but won't be # properly set until later - self.updater: update.Updater = None + self.updater: update.Updater | None = None self.menubar = tk.Menu() if sys.platform == 'darwin': @@ -647,7 +647,9 @@ class AppWindow(object): self.help_menu.add_command(command=self.help_general) self.help_menu.add_command(command=self.help_privacy) self.help_menu.add_command(command=self.help_releases) - self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) + if self.updater is not None: + self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) + self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w)) self.menubar.add_cascade(menu=self.help_menu) @@ -1005,6 +1007,8 @@ class AppWindow(object): :param event: Tk generated event details. """ logger.trace_if('capi.worker', 'Begin') + should_return: bool + new_data: Dict[str, Any] should_return, new_data = killswitch.check_killswitch('capi.auth', {}) if should_return: logger.warning('capi.auth has been disabled via killswitch. Returning.') diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index 43071732..44cd38b0 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object): # type: ignore """Clickable label for HTTP links.""" - def __init__(self, master: Optional[tk.Tk] = None, **kw: Any) -> None: + def __init__(self, master: tk.Frame | None = None, **kw: Any) -> None: self.url = 'url' in kw and kw.pop('url') or None self.popup_copy = kw.pop('popup_copy', False) self.underline = kw.pop('underline', None) # override ttk.Label's underline From 69d764c0274ab693a5bc61050a12c7e90574435c Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 17:06:24 +0000 Subject: [PATCH 40/72] EDMarketConnector: More misc type fixes --- EDMarketConnector.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 11f103b4..58d075a8 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -1425,7 +1425,7 @@ class AppWindow(object): config.set('cmdrs', config.get_list('cmdrs', default=[]) + [monitor.cmdr]) self.login() - if monitor.mode == 'CQC' and entry['event']: + if monitor.cmdr and monitor.mode == 'CQC' and entry['event']: err = plug.notify_journal_entry_cqc(monitor.cmdr, monitor.is_beta, entry, monitor.state) if err: self.status['text'] = err @@ -1443,7 +1443,9 @@ class AppWindow(object): # Disable WinSparkle automatic update checks, IFF configured to do so when in-game if config.get_int('disable_autoappupdatecheckingame') and 1: - self.updater.set_automatic_updates_check(False) + if self.updater is not None: + self.updater.set_automatic_updates_check(False) + logger.info('Monitor: Disable WinSparkle automatic update checks') # Can't start dashboard monitoring @@ -1455,12 +1457,16 @@ class AppWindow(object): and config.get_int('output') & config.OUT_SHIP: monitor.export_ship() - err = plug.notify_journal_entry(monitor.cmdr, - monitor.is_beta, - monitor.system, - monitor.station, - entry, - monitor.state) + if monitor.cmdr and monitor.system and monitor.station: + err = plug.notify_journal_entry( + monitor.cmdr, + monitor.is_beta, + monitor.system, + monitor.station, + entry, + monitor.state + ) + if err: self.status['text'] = err if not config.get_int('hotkey_mute'): @@ -1496,7 +1502,9 @@ class AppWindow(object): if entry['event'] == 'ShutDown': # Enable WinSparkle automatic update checks # NB: Do this blindly, in case option got changed whilst in-game - self.updater.set_automatic_updates_check(True) + if self.updater is not None: + self.updater.set_automatic_updates_check(True) + logger.info('Monitor: Enable WinSparkle automatic update checks') def auth(self, event=None) -> None: @@ -1539,7 +1547,9 @@ class AppWindow(object): entry = dashboard.status # Currently we don't do anything with these events - err = plug.notify_dashboard_entry(monitor.cmdr, monitor.is_beta, entry) + if monitor.cmdr: + err = plug.notify_dashboard_entry(monitor.cmdr, monitor.is_beta, entry) + if err: self.status['text'] = err if not config.get_int('hotkey_mute'): From 4de83747f89988446bd31c743ec41751ff55677f Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 17:18:58 +0000 Subject: [PATCH 41/72] EDMarketConnector: More minor type fixes Also, the killswitch popup ends up un-themed *anyway*, so don't even call `theme.apply()`. That function expects a `tk.Tk` not, `Toplevel`, and doesn't even do anything for a `Toplevel` anyway. --- EDMarketConnector.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 58d075a8..388659ee 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -1557,13 +1557,13 @@ class AppWindow(object): def plugin_error(self, event=None) -> None: """Display asynchronous error from plugin.""" - if plug.last_error.get('msg'): - self.status['text'] = plug.last_error['msg'] + if plug.last_error.msg: + self.status['text'] = plug.last_error.msg self.w.update_idletasks() if not config.get_int('hotkey_mute'): hotkeymgr.play_bad() - def shipyard_url(self, shipname: str) -> str: + def shipyard_url(self, shipname: str) -> str | None: """Despatch a ship URL to the configured handler.""" if not (loadout := monitor.ship()): logger.warning('No ship loadout, aborting.') @@ -1590,11 +1590,11 @@ class AppWindow(object): return f'file://localhost/{file_name}' - def system_url(self, system: str) -> str: + def system_url(self, system: str) -> str | None: """Despatch a system URL to the configured handler.""" return plug.invoke(config.get_str('system_provider'), 'EDSM', 'system_url', monitor.system) - def station_url(self, station: str) -> str: + def station_url(self, station: str) -> str | None: """Despatch a station URL to the configured handler.""" return plug.invoke(config.get_str('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station) @@ -1671,7 +1671,7 @@ class AppWindow(object): self.resizable(tk.FALSE, tk.FALSE) - frame = ttk.Frame(self) + frame = tk.Frame(self) frame.grid(sticky=tk.NSEW) row = 1 @@ -1684,7 +1684,7 @@ class AppWindow(object): ############################################################ # version - ttk.Label(frame).grid(row=row, column=0) # spacer + tk.Label(frame).grid(row=row, column=0) # spacer row += 1 self.appversion_label = tk.Label(frame, text=appversion()) self.appversion_label.grid(row=row, column=0, sticky=tk.E) @@ -1792,7 +1792,8 @@ class AppWindow(object): # First so it doesn't interrupt us logger.info('Closing update checker...') - self.updater.close() + if self.updater is not None: + self.updater.close() # Earlier than anything else so plugin code can't interfere *and* it # won't still be running in a manner that might rely on something @@ -1941,11 +1942,9 @@ def show_killswitch_poppup(root=None): idx += 1 idx += 1 - ok_button = tk.Button(frame, text="ok", command=tl.destroy) + ok_button = tk.Button(frame, text="Ok", command=tl.destroy) ok_button.grid(columnspan=2, sticky=tk.EW) - theme.apply(tl) - # Run the app if __name__ == "__main__": # noqa: C901 From eaaa6fead08d95b6b58a5c24ca2c7e12cebf5ddd Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 17:25:13 +0000 Subject: [PATCH 42/72] Correctly type `theme` ui_scale variables Technically `theme.startup_ui_scale` should be `float` to match with `default_ui_scale` from tkinter, but we store it in the config as `int`, so go with that. --- EDMarketConnector.py | 5 ++++- theme.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 388659ee..c5831e74 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -2089,10 +2089,13 @@ sys.path: {sys.path}''' if not ui_scale: ui_scale = 100 config.set('ui_scale', ui_scale) + theme.default_ui_scale = root.tk.call('tk', 'scaling') logger.trace_if('tk', f'Default tk scaling = {theme.default_ui_scale}') theme.startup_ui_scale = ui_scale - root.tk.call('tk', 'scaling', theme.default_ui_scale * float(ui_scale) / 100.0) + if theme.default_ui_scale is not None: + root.tk.call('tk', 'scaling', theme.default_ui_scale * float(ui_scale) / 100.0) + app = AppWindow(root) def messagebox_not_py3(): diff --git a/theme.py b/theme.py index 45886f16..b5d51484 100644 --- a/theme.py +++ b/theme.py @@ -137,8 +137,8 @@ class _Theme(object): self.widgets_pair: List = [] self.defaults: Dict = {} self.current: Dict = {} - self.default_ui_scale = None # None == not yet known - self.startup_ui_scale = None + self.default_ui_scale: float | None = None # None == not yet known + self.startup_ui_scale: int | None = None def register(self, widget: tk.Widget | tk.BitmapImage) -> None: # noqa: CCR001, C901 # Note widget and children for later application of a theme. Note if From 63a1337bedfd8839768b4dca298a2f56cc3356f0 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 17:37:55 +0000 Subject: [PATCH 43/72] examples/click_counter: Minor type fixes. --- docs/examples/click_counter/load.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/examples/click_counter/load.py b/docs/examples/click_counter/load.py index b1ea6ac5..70c24f53 100644 --- a/docs/examples/click_counter/load.py +++ b/docs/examples/click_counter/load.py @@ -6,7 +6,6 @@ It adds a single button to the EDMC interface that displays the number of times import logging import tkinter as tk -import tkinter.ttk as ttk from typing import Optional import myNotebook as nb # noqa: N813 @@ -49,7 +48,7 @@ class ClickCounter: """ self.on_preferences_closed("", False) # Save our prefs - def setup_preferences(self, parent: ttk.Frame, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: + def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: """ setup_preferences is called by plugin_prefs below. @@ -128,7 +127,7 @@ def plugin_stop() -> None: return cc.on_unload() -def plugin_prefs(parent: ttk.Frame, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: +def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: """ Handle preferences tab for the plugin. From 46d518986ca52a90f1b58c11dc6146d974bce83d Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 17:41:01 +0000 Subject: [PATCH 44/72] hotkey/windows: Minor formatting cleanups --- hotkey/windows.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/hotkey/windows.py b/hotkey/windows.py index 4dd110af..27faba30 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -62,6 +62,7 @@ GetWindowText = ctypes.windll.user32.GetWindowTextW GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW + def window_title(h) -> str: """ Determine the title for a window. @@ -77,6 +78,7 @@ def window_title(h) -> str: return '' + class MOUSEINPUT(ctypes.Structure): """Mouse Input structure.""" @@ -89,6 +91,7 @@ class MOUSEINPUT(ctypes.Structure): ('dwExtraInfo', ctypes.POINTER(ULONG)) ] + class KEYBDINPUT(ctypes.Structure): """Keyboard Input structure.""" @@ -100,6 +103,7 @@ class KEYBDINPUT(ctypes.Structure): ('dwExtraInfo', ctypes.POINTER(ULONG)) ] + class HARDWAREINPUT(ctypes.Structure): """Hardware Input structure.""" @@ -109,6 +113,7 @@ class HARDWAREINPUT(ctypes.Structure): ('wParamH', WORD) ] + class INPUTUNION(ctypes.Union): """Input union.""" @@ -118,6 +123,7 @@ class INPUTUNION(ctypes.Union): ('hi', HARDWAREINPUT) ] + class INPUT(ctypes.Structure): """Input structure.""" @@ -126,6 +132,7 @@ class INPUT(ctypes.Structure): ('union', INPUTUNION) ] + SendInput = ctypes.windll.user32.SendInput SendInput.argtypes = [ctypes.c_uint, ctypes.POINTER(INPUT), ctypes.c_int] @@ -268,10 +275,10 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): :return: False to retain previous, None to not use, else (keycode, modifiers) """ modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ - | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ - | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ - | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ - | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) + | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ + | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ + | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ + | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) keycode = event.keycode if keycode in [VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN]: @@ -284,8 +291,9 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): elif keycode in [VK_BACK, VK_DELETE, VK_CLEAR, VK_OEM_CLEAR]: # BkSp, Del, Clear = clear hotkey return None - elif keycode in [VK_RETURN, VK_SPACE, VK_OEM_MINUS] or ord('A') <= keycode <= ord( - 'Z'): # don't allow keys needed for typing in System Map + elif ( + keycode in [VK_RETURN, VK_SPACE, VK_OEM_MINUS] or ord('A') <= keycode <= ord('Z') + ): # don't allow keys needed for typing in System Map winsound.MessageBeep() return None From 5609b908fb625c344c1427ece137415e2d775de3 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 22 Dec 2022 17:41:35 +0000 Subject: [PATCH 45/72] ttkHyperlinkLabel: Remove un-used typing.Optional import --- ttkHyperlinkLabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index 44cd38b0..1d9749ac 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -19,7 +19,7 @@ import tkinter as tk import webbrowser from tkinter import font as tk_font from tkinter import ttk -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: def _(x: str) -> str: ... From b3719347e8aac462cdf7f2829b61265d8cf05f53 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 11:50:15 +0000 Subject: [PATCH 46/72] EDMarketConnector: No need for Typing.Dict --- EDMarketConnector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index c5831e74..b6bbcf11 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -16,7 +16,7 @@ from builtins import object, str from os import chdir, environ from os.path import dirname, join from time import localtime, strftime, time -from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Literal, Optional, Tuple, Union # Have this as early as possible for people running EDMarketConnector.exe # from cmd.exe or a bat file or similar. Else they might not be in the correct @@ -1008,7 +1008,7 @@ class AppWindow(object): """ logger.trace_if('capi.worker', 'Begin') should_return: bool - new_data: Dict[str, Any] + new_data: dict[str, Any] should_return, new_data = killswitch.check_killswitch('capi.auth', {}) if should_return: logger.warning('capi.auth has been disabled via killswitch. Returning.') From 1e0d99a61f5e605d14433d4455a0251987cf37b9 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 12:14:54 +0000 Subject: [PATCH 47/72] config/__init__: Remove use of typing.Union --- config/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index ffc635e3..ae594c53 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -40,7 +40,7 @@ import sys import traceback import warnings from abc import abstractmethod -from typing import Any, Callable, List, Optional, Type, TypeVar, Union +from typing import Any, Callable, List, Optional, Type, TypeVar import semantic_version @@ -293,7 +293,7 @@ class AbstractConfig(abc.ABC): @staticmethod def _suppress_call( - func: Callable[..., _T], exceptions: Union[Type[BaseException], List[Type[BaseException]]] = Exception, + func: Callable[..., _T], exceptions: Type[BaseException] | List[Type[BaseException]] = Exception, *args: Any, **kwargs: Any ) -> Optional[_T]: if exceptions is None: @@ -309,8 +309,8 @@ class AbstractConfig(abc.ABC): def get( self, key: str, - default: Union[list, str, bool, int, None] = None - ) -> Union[list, str, bool, int, None]: + default: list | str | bool | int | None = None + ) -> list | str | bool | int | None: """ Return the data for the requested key, or a default. @@ -399,7 +399,7 @@ class AbstractConfig(abc.ABC): raise NotImplementedError @abstractmethod - def set(self, key: str, val: Union[int, str, List[str], bool]) -> None: + def set(self, key: str, val: int | str | List[str] | bool) -> None: """ Set the given key's data to the given value. From 299b42c55804c676959c15f2cbb91b28d1616838 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 12:15:25 +0000 Subject: [PATCH 48/72] config/__init__: Remove use of typing.List --- config/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index ae594c53..ae2fad73 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -40,7 +40,7 @@ import sys import traceback import warnings from abc import abstractmethod -from typing import Any, Callable, List, Optional, Type, TypeVar +from typing import Any, Callable, Optional, Type, TypeVar import semantic_version @@ -59,10 +59,10 @@ copyright = '© 2015-2019 Jonathan Harris, 2020-2022 EDCD' update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml' update_interval = 8*60*60 # Providers marked to be in debug mode. Generally this is expected to switch to sending data to a log file -debug_senders: List[str] = [] +debug_senders: list[str] = [] # TRACE logging code that should actually be used. Means not spamming it # *all* if only interested in some things. -trace_on: List[str] = [] +trace_on: list[str] = [] capi_pretend_down: bool = False capi_debug_access_token: Optional[str] = None @@ -293,7 +293,7 @@ class AbstractConfig(abc.ABC): @staticmethod def _suppress_call( - func: Callable[..., _T], exceptions: Type[BaseException] | List[Type[BaseException]] = Exception, + func: Callable[..., _T], exceptions: Type[BaseException] | list[Type[BaseException]] = Exception, *args: Any, **kwargs: Any ) -> Optional[_T]: if exceptions is None: @@ -399,7 +399,7 @@ class AbstractConfig(abc.ABC): raise NotImplementedError @abstractmethod - def set(self, key: str, val: int | str | List[str] | bool) -> None: + def set(self, key: str, val: int | str | list[str] | bool) -> None: """ Set the given key's data to the given value. From fe5f68763b7c04b248a6651d85c9ffe9dcfcfc40 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 12:16:31 +0000 Subject: [PATCH 49/72] docs/Releasing: Remove paste error --- docs/Releasing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Releasing.md b/docs/Releasing.md index 7524ebbb..a8c4dc80 100644 --- a/docs/Releasing.md +++ b/docs/Releasing.md @@ -166,7 +166,7 @@ that it's actually included in the installer. ``` - | None + Note that you only need `Id=""` if the filename itself is not a valid Id, e.g. because it contains spaces. From 8a7a0fdf9a85b6dd47016b66f81bcb483478799d Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 12:17:40 +0000 Subject: [PATCH 50/72] hotkey/windows: Remove un-necessary `u` tagging of strings --- hotkey/windows.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/hotkey/windows.py b/hotkey/windows.py index 27faba30..0bc54d1a 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -147,14 +147,14 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): # https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx # Limit ourselves to symbols in Windows 7 Segoe UI DISPLAY = { - 0x03: 'Break', 0x08: 'Bksp', 0x09: u'↹', 0x0c: 'Clear', 0x0d: u'↵', 0x13: 'Pause', - 0x14: u'Ⓐ', 0x1b: 'Esc', - 0x20: u'⏘', 0x21: 'PgUp', 0x22: 'PgDn', 0x23: 'End', 0x24: 'Home', - 0x25: u'←', 0x26: u'↑', 0x27: u'→', 0x28: u'↓', + 0x03: 'Break', 0x08: 'Bksp', 0x09: '↹', 0x0c: 'Clear', 0x0d: '↵', 0x13: 'Pause', + 0x14: 'Ⓐ', 0x1b: 'Esc', + 0x20: '⏘', 0x21: 'PgUp', 0x22: 'PgDn', 0x23: 'End', 0x24: 'Home', + 0x25: '←', 0x26: '↑', 0x27: '→', 0x28: '↓', 0x2c: 'PrtScn', 0x2d: 'Ins', 0x2e: 'Del', 0x2f: 'Help', - 0x5d: u'▤', 0x5f: u'☾', - 0x90: u'➀', 0x91: 'ScrLk', - 0xa6: u'⇦', 0xa7: u'⇨', 0xa9: u'⊗', 0xab: u'☆', 0xac: u'⌂', 0xb4: u'✉', + 0x5d: '▤', 0x5f: '☾', + 0x90: '➀', 0x91: 'ScrLk', + 0xa6: '⇦', 0xa7: '⇨', 0xa9: '⊗', 0xab: '☆', 0xac: '⌂', 0xb4: '✉', } def __init__(self) -> None: @@ -320,19 +320,19 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): """ text = '' if modifiers & MOD_WIN: - text += u'❖+' + text += '❖+' if modifiers & MOD_CONTROL: - text += u'Ctrl+' + text += 'Ctrl+' if modifiers & MOD_ALT: - text += u'Alt+' + text += 'Alt+' if modifiers & MOD_SHIFT: - text += u'⇧+' + text += '⇧+' if VK_NUMPAD0 <= keycode <= VK_DIVIDE: - text += u'№' + text += '№' if not keycode: pass @@ -346,7 +346,7 @@ class WindowsHotkeyMgr(AbstractHotkeyMgr): else: c = MapVirtualKey(keycode, 2) # printable ? if not c: # oops not printable - text += u'⁈' + text += '⁈' elif c < 0x20: # control keys text += chr(c + 0x40) From 5e19d3e9aa1103dd752017df2dc2fca41e0285f1 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 12:20:54 +0000 Subject: [PATCH 51/72] plugins/eddn: No need for typing.(Dict|List) usage --- plugins/eddn.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/plugins/eddn.py b/plugins/eddn.py index 3cd2d819..f6591779 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -34,7 +34,7 @@ from collections import OrderedDict from platform import system from textwrap import dedent from threading import Lock -from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Mapping, MutableMapping, Optional +from typing import TYPE_CHECKING, Any, Iterator, Mapping, MutableMapping, Optional from typing import OrderedDict as OrderedDictT from typing import Tuple, Union @@ -91,13 +91,13 @@ class This: # Avoid duplicates self.marketId: Optional[str] = None - self.commodities: Optional[List[OrderedDictT[str, Any]]] = None - self.outfitting: Optional[Tuple[bool, List[str]]] = None - self.shipyard: Optional[Tuple[bool, List[Mapping[str, Any]]]] = None + self.commodities: Optional[list[OrderedDictT[str, Any]]] = None + self.outfitting: Optional[Tuple[bool, list[str]]] = None + self.shipyard: Optional[Tuple[bool, list[Mapping[str, Any]]]] = None self.fcmaterials_marketid: int = 0 - self.fcmaterials: Optional[List[OrderedDictT[str, Any]]] = None + self.fcmaterials: Optional[list[OrderedDictT[str, Any]]] = None self.fcmaterials_capi_marketid: int = 0 - self.fcmaterials_capi: Optional[List[OrderedDictT[str, Any]]] = None + self.fcmaterials_capi: Optional[list[OrderedDictT[str, Any]]] = None # For the tkinter parent window, so we can call update_idletasks() self.parent: tk.Tk @@ -612,7 +612,7 @@ class EDDN: self.sender = EDDNSender(self, self.eddn_url) - self.fss_signals: List[Mapping[str, Any]] = [] + self.fss_signals: list[Mapping[str, Any]] = [] def close(self): """Close down the EDDN class instance.""" @@ -636,7 +636,7 @@ class EDDN: :param is_beta: whether or not we're currently in beta mode """ should_return: bool - new_data: Dict[str, Any] + new_data: dict[str, Any] should_return, new_data = killswitch.check_killswitch('capi.request./market', {}) if should_return: logger.warning("capi.request./market has been disabled by killswitch. Returning.") @@ -653,7 +653,7 @@ class EDDN: modules, ships ) - commodities: List[OrderedDictT[str, Any]] = [] + commodities: list[OrderedDictT[str, Any]] = [] for commodity in data['lastStarport'].get('commodities') or []: # Check 'marketable' and 'not prohibited' if (category_map.get(commodity['categoryname'], True) @@ -715,7 +715,7 @@ class EDDN: # Send any FCMaterials.json-equivalent 'orders' as well self.export_capi_fcmaterials(data, is_beta, horizons) - def safe_modules_and_ships(self, data: Mapping[str, Any]) -> Tuple[Dict, Dict]: + def safe_modules_and_ships(self, data: Mapping[str, Any]) -> Tuple[dict, dict]: """ Produce a sanity-checked version of ships and modules from CAPI data. @@ -726,7 +726,7 @@ class EDDN: :param data: The raw CAPI data. :return: Sanity-checked data. """ - modules: Dict[str, Any] = data['lastStarport'].get('modules') + modules: dict[str, Any] = data['lastStarport'].get('modules') if modules is None or not isinstance(modules, dict): if modules is None: logger.debug('modules was None. FC or Damaged Station?') @@ -743,7 +743,7 @@ class EDDN: # Set a safe value modules = {} - ships: Dict[str, Any] = data['lastStarport'].get('ships') + ships: dict[str, Any] = data['lastStarport'].get('ships') if ships is None or not isinstance(ships, dict): if ships is None: logger.debug('ships was None') @@ -769,7 +769,7 @@ class EDDN: :param is_beta: whether or not we're currently in beta mode """ should_return: bool - new_data: Dict[str, Any] + new_data: dict[str, Any] should_return, new_data = killswitch.check_killswitch('capi.request./shipyard', {}) if should_return: logger.warning("capi.request./shipyard has been disabled by killswitch. Returning.") @@ -796,7 +796,7 @@ class EDDN: modules.values() ) - outfitting: List[str] = sorted( + outfitting: list[str] = sorted( self.MODULE_RE.sub(lambda match: match.group(0).capitalize(), mod['name'].lower()) for mod in to_search ) @@ -837,7 +837,7 @@ class EDDN: :param is_beta: whether or not we are in beta mode """ should_return: bool - new_data: Dict[str, Any] + new_data: dict[str, Any] should_return, new_data = killswitch.check_killswitch('capi.request./shipyard', {}) if should_return: logger.warning("capi.request./shipyard has been disabled by killswitch. Returning.") @@ -856,7 +856,7 @@ class EDDN: ships ) - shipyard: List[Mapping[str, Any]] = sorted( + shipyard: list[Mapping[str, Any]] = sorted( itertools.chain( (ship['name'].lower() for ship in (ships['shipyard_list'] or {}).values()), (ship['name'].lower() for ship in ships['unavailable_list'] or {}), @@ -899,8 +899,8 @@ class EDDN: :param is_beta: whether or not we're in beta mode :param entry: the journal entry containing the commodities data """ - items: List[Mapping[str, Any]] = entry.get('Items') or [] - commodities: List[OrderedDictT[str, Any]] = sorted((OrderedDict([ + items: list[Mapping[str, Any]] = entry.get('Items') or [] + commodities: list[OrderedDictT[str, Any]] = sorted((OrderedDict([ ('name', self.canonicalise(commodity['Name'])), ('meanPrice', commodity['MeanPrice']), ('buyPrice', commodity['BuyPrice']), @@ -947,11 +947,11 @@ class EDDN: :param is_beta: Whether or not we're in beta mode :param entry: The relevant journal entry """ - modules: List[Mapping[str, Any]] = entry.get('Items', []) + modules: list[Mapping[str, Any]] = entry.get('Items', []) horizons: bool = entry.get('Horizons', False) # outfitting = sorted([self.MODULE_RE.sub(lambda m: m.group(0).capitalize(), module['Name']) # for module in modules if module['Name'] != 'int_planetapproachsuite']) - outfitting: List[str] = sorted( + outfitting: list[str] = sorted( self.MODULE_RE.sub(lambda m: m.group(0).capitalize(), mod['Name']) for mod in filter(lambda m: m['Name'] != 'int_planetapproachsuite', modules) ) @@ -986,7 +986,7 @@ class EDDN: :param is_beta: Whether or not we're in beta mode :param entry: the relevant journal entry """ - ships: List[Mapping[str, Any]] = entry.get('PriceList') or [] + ships: list[Mapping[str, Any]] = entry.get('PriceList') or [] horizons: bool = entry.get('Horizons', False) shipyard = sorted(ship['ShipType'] for ship in ships) # Don't send empty ships list - shipyard data is only guaranteed present if user has visited the shipyard. @@ -1828,7 +1828,7 @@ class EDDN: ####################################################################### # Build basis of message - msg: Dict = { + msg: dict = { '$schemaRef': f'https://eddn.edcd.io/schemas/fsssignaldiscovered/1{"/test" if is_beta else ""}', 'message': { "event": "FSSSignalDiscovered", @@ -2576,7 +2576,7 @@ def capi_is_horizons(economies: MAP_STR_ANY, modules: MAP_STR_ANY, ships: MAP_ST return economies_colony or modules_horizons or ship_horizons -def dashboard_entry(cmdr: str, is_beta: bool, entry: Dict[str, Any]) -> None: +def dashboard_entry(cmdr: str, is_beta: bool, entry: dict[str, Any]) -> None: """ Process Status.json data to track things like current Body. From bc5cb48f8cfeccbde8b4092278541f27dc22a783 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 12:25:36 +0000 Subject: [PATCH 52/72] stats.py: Remove usage of un-necessary typing types. Dict, List, Optional --- stats.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/stats.py b/stats.py index f7f15b86..6b5d8d61 100644 --- a/stats.py +++ b/stats.py @@ -5,7 +5,7 @@ import sys import tkinter import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Any, AnyStr, Callable, Dict, List, NamedTuple, Optional, Sequence, cast +from typing import TYPE_CHECKING, Any, AnyStr, Callable, NamedTuple, Sequence, cast import companion import EDMCLogging @@ -45,7 +45,7 @@ RANK_LINES_END = 9 POWERPLAY_LINES_START = 9 -def status(data: Dict[str, Any]) -> List[List[str]]: +def status(data: dict[str, Any]) -> list[list[str]]: """ Get the current status of the cmdr referred to by data. @@ -211,7 +211,7 @@ def status(data: Dict[str, Any]) -> List[List[str]]: return res -def export_status(data: Dict[str, Any], filename: AnyStr) -> None: +def export_status(data: dict[str, Any], filename: AnyStr) -> None: """ Export status data to a CSV file. @@ -236,21 +236,21 @@ class ShipRet(NamedTuple): value: str -def ships(companion_data: Dict[str, Any]) -> List[ShipRet]: +def ships(companion_data: dict[str, Any]) -> list[ShipRet]: """ Return a list of 5 tuples of ship information. :param data: [description] :return: A 5 tuple of strings containing: Ship ID, Ship Type Name (internal), Ship Name, System, Station, and Value """ - ships: List[Dict[str, Any]] = companion.listify(cast(list, companion_data.get('ships'))) + ships: list[dict[str, Any]] = companion.listify(cast(list, companion_data.get('ships'))) current = companion_data['commander'].get('currentShipId') if isinstance(current, int) and current < len(ships) and ships[current]: ships.insert(0, ships.pop(current)) # Put current ship first if not companion_data['commander'].get('docked'): - out: List[ShipRet] = [] + out: list[ShipRet] = [] # Set current system, not last docked out.append(ShipRet( id=str(ships[0]['id']), @@ -285,7 +285,7 @@ def ships(companion_data: Dict[str, Any]) -> List[ShipRet]: ] -def export_ships(companion_data: Dict[str, Any], filename: AnyStr) -> None: +def export_ships(companion_data: dict[str, Any], filename: AnyStr) -> None: """ Export the current ships to a CSV file. @@ -358,7 +358,7 @@ class StatsDialog(): class StatsResults(tk.Toplevel): """Status window.""" - def __init__(self, parent: tk.Tk, data: Dict[str, Any]) -> None: + def __init__(self, parent: tk.Tk, data: dict[str, Any]) -> None: tk.Toplevel.__init__(self, parent) self.parent = parent @@ -443,7 +443,7 @@ class StatsResults(tk.Toplevel): self.geometry(f"+{position.left}+{position.top}") def addpage( - self, parent, header: List[str] | None = None, align: str | None = None + self, parent, header: list[str] | None = None, align: str | None = None ) -> ttk.Frame: """ Add a page to the StatsResults screen. @@ -464,7 +464,7 @@ class StatsResults(tk.Toplevel): return page - def addpageheader(self, parent: ttk.Frame, header: Sequence[str], align: Optional[str] = None) -> None: + def addpageheader(self, parent: ttk.Frame, header: Sequence[str], align: str | None = None) -> None: """ Add the column headers to the page, followed by a separator. @@ -480,7 +480,7 @@ class StatsResults(tk.Toplevel): self.addpagerow(parent, ['']) def addpagerow( - self, parent: ttk.Frame, content: Sequence[str], align: Optional[str] = None, with_copy: bool = False + self, parent: ttk.Frame, content: Sequence[str], align: str | None = None, with_copy: bool = False ): """ Add a single row to parent. From 5064b10744976a9ffe9b084e6a1f951566e7fdd0 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 12:32:09 +0000 Subject: [PATCH 53/72] Bring in `scripts/pip_rev_deps.py` * This finds the pip-installed modules that depend on the specified module. Handy for cleaning things up. * Leads to needing types-pkg-resources for mypy (and via pre-commit). --- .pre-commit-config.yaml | 2 +- requirements-dev.txt | 1 + scripts/pip_rev_deps.py | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 scripts/pip_rev_deps.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 778c2161..5b534279 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,7 +51,7 @@ repos: - id: mypy # verbose: true # log_file: 'pre-commit_mypy.log' - additional_dependencies: [ types-requests, types-urllib3 ] + additional_dependencies: [ types-pkg-resources, types-requests, types-urllib3 ] # args: [ "--follow-imports", "skip", "--ignore-missing-imports", "--scripts-are-modules" ] ### # pydocstyle.exe diff --git a/requirements-dev.txt b/requirements-dev.txt index ceacac8e..cb25fbf8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,6 +24,7 @@ mypy==0.991 pep8-naming==0.13.3 safety==2.3.5 types-requests==2.28.11.7 +types-pkg-resources==0.1.3 # Code formatting tools autopep8==2.0.1 diff --git a/scripts/pip_rev_deps.py b/scripts/pip_rev_deps.py new file mode 100644 index 00000000..b97757d5 --- /dev/null +++ b/scripts/pip_rev_deps.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Find the reverse dependencies of a package according to pip.""" +import sys + +import pkg_resources + + +def find_reverse_deps(package_name: str): + """ + Find the packages that depend on the named one. + + :param package_name: Target package. + :return: List of packages that depend on this one. + """ + return [ + pkg.project_name for pkg in pkg_resources.WorkingSet() + if package_name in {req.project_name for req in pkg.requires()} + ] + + +if __name__ == '__main__': + print(find_reverse_deps(sys.argv[1])) From 90183554ef157f8a508d40a6979753044be2909c Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 12:59:44 +0000 Subject: [PATCH 54/72] tests/journal_lock: Use proper `pytest` types in fixtures When I last typed this I couldn't find any official way to type `pytest` fixtures. Perhaps that was before: https://docs.pytest.org/en/6.2.x/changelog.html#improvements which now makes it clean and simple. --- tests/journal_lock.py/test_journal_lock.py | 39 +++++++++++----------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/tests/journal_lock.py/test_journal_lock.py b/tests/journal_lock.py/test_journal_lock.py index 7bfd4ea8..c617d52c 100644 --- a/tests/journal_lock.py/test_journal_lock.py +++ b/tests/journal_lock.py/test_journal_lock.py @@ -3,11 +3,10 @@ import multiprocessing as mp import os import pathlib import sys +from typing import Generator import pytest -# Import as other names else they get picked up when used as fixtures -from _pytest import monkeypatch as _pytest_monkeypatch -from _pytest import tmpdir as _pytest_tmpdir +from pytest import MonkeyPatch, TempdirFactory, TempPathFactory from config import config from journal_lock import JournalLock, JournalLockResult @@ -117,9 +116,9 @@ class TestJournalLock: @pytest.fixture def mock_journaldir( - self, monkeypatch: _pytest_monkeypatch, - tmp_path_factory: _pytest_tmpdir.TempPathFactory - ) -> _pytest_tmpdir.TempPathFactory: + self, monkeypatch: MonkeyPatch, + tmp_path_factory: TempdirFactory + ) -> Generator: """Fixture for mocking config.get_str('journaldir').""" def get_str(key: str, *, default: str | None = None) -> str: """Mock config.*Config get_str to provide fake journaldir.""" @@ -136,9 +135,9 @@ class TestJournalLock: @pytest.fixture def mock_journaldir_changing( self, - monkeypatch: _pytest_monkeypatch, - tmp_path_factory: _pytest_tmpdir.TempPathFactory - ) -> _pytest_tmpdir.TempPathFactory: + monkeypatch: MonkeyPatch, + tmp_path_factory: TempdirFactory + ) -> Generator: """Fixture for mocking config.get_str('journaldir').""" def get_str(key: str, *, default: str | None = None) -> str: """Mock config.*Config get_str to provide fake journaldir.""" @@ -154,7 +153,7 @@ class TestJournalLock: ########################################################################### # Tests against JournalLock.__init__() - def test_journal_lock_init(self, mock_journaldir: _pytest_tmpdir.TempPathFactory): + def test_journal_lock_init(self, mock_journaldir: TempPathFactory): """Test JournalLock instantiation.""" print(f'{type(mock_journaldir)=}') tmpdir = str(mock_journaldir.getbasetemp()) @@ -176,14 +175,14 @@ class TestJournalLock: jlock.set_path_from_journaldir() assert jlock.journal_dir_path is None - def test_path_from_journaldir_with_tmpdir(self, mock_journaldir: _pytest_tmpdir.TempPathFactory): + def test_path_from_journaldir_with_tmpdir(self, mock_journaldir: TempPathFactory): """Test JournalLock.set_path_from_journaldir() with tmpdir.""" tmpdir = mock_journaldir jlock = JournalLock() # Check that an actual journaldir is handled correctly. - jlock.journal_dir = tmpdir + jlock.journal_dir = str(tmpdir) jlock.set_path_from_journaldir() assert isinstance(jlock.journal_dir_path, pathlib.Path) @@ -200,7 +199,7 @@ class TestJournalLock: locked = jlock.obtain_lock() assert locked == JournalLockResult.JOURNALDIR_IS_NONE - def test_obtain_lock_with_tmpdir(self, mock_journaldir: _pytest_tmpdir.TempPathFactory): + def test_obtain_lock_with_tmpdir(self, mock_journaldir: TempPathFactory): """Test JournalLock.obtain_lock() with tmpdir.""" jlock = JournalLock() @@ -213,7 +212,7 @@ class TestJournalLock: assert jlock.release_lock() os.unlink(str(jlock.journal_dir_lockfile_name)) - def test_obtain_lock_with_tmpdir_ro(self, mock_journaldir: _pytest_tmpdir.TempPathFactory): + def test_obtain_lock_with_tmpdir_ro(self, mock_journaldir: TempPathFactory): """Test JournalLock.obtain_lock() with read-only tmpdir.""" tmpdir = str(mock_journaldir.getbasetemp()) print(f'{tmpdir=}') @@ -280,7 +279,7 @@ class TestJournalLock: assert locked == JournalLockResult.JOURNALDIR_READONLY - def test_obtain_lock_already_locked(self, mock_journaldir: _pytest_tmpdir.TempPathFactory): + def test_obtain_lock_already_locked(self, mock_journaldir: TempPathFactory): """Test JournalLock.obtain_lock() with tmpdir.""" continue_q: mp.Queue = mp.Queue() exit_q: mp.Queue = mp.Queue() @@ -312,7 +311,7 @@ class TestJournalLock: ########################################################################### # Tests against JournalLock.release_lock() - def test_release_lock(self, mock_journaldir: _pytest_tmpdir.TempPathFactory): + def test_release_lock(self, mock_journaldir: TempPathFactory): """Test JournalLock.release_lock().""" # First actually obtain the lock, and check it worked jlock = JournalLock() @@ -330,12 +329,12 @@ class TestJournalLock: # Cleanup, to avoid side-effect on other tests os.unlink(str(jlock.journal_dir_lockfile_name)) - def test_release_lock_not_locked(self, mock_journaldir: _pytest_tmpdir.TempPathFactory): + def test_release_lock_not_locked(self, mock_journaldir: TempPathFactory): """Test JournalLock.release_lock() when not locked.""" jlock = JournalLock() assert jlock.release_lock() - def test_release_lock_lie_locked(self, mock_journaldir: _pytest_tmpdir.TempPathFactory): + def test_release_lock_lie_locked(self, mock_journaldir: TempPathFactory): """Test JournalLock.release_lock() when not locked, but lie we are.""" jlock = JournalLock() jlock.locked = True @@ -345,7 +344,7 @@ class TestJournalLock: # Tests against JournalLock.update_lock() def test_update_lock( self, - mock_journaldir_changing: _pytest_tmpdir.TempPathFactory): + mock_journaldir_changing: TempPathFactory): """ Test JournalLock.update_lock(). @@ -373,7 +372,7 @@ class TestJournalLock: # And the old_journaldir's lockfile too os.unlink(str(old_journaldir_lockfile_name)) - def test_update_lock_same(self, mock_journaldir: _pytest_tmpdir.TempPathFactory): + def test_update_lock_same(self, mock_journaldir: TempPathFactory): """ Test JournalLock.update_lock(). From a499a22383521f6f47abb3015de966812d771e68 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 13:02:40 +0000 Subject: [PATCH 55/72] .mypy.ini: Set `follow_imports` explicitly to `normal` default We'd been using `skip` because when we started with mypy next to nothing passed it, and checking one file would spew errors with other files and it just wasn't conducive to making at least *some* progress. But now we should have every single file passing, so this is the right thing to do. --- .mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mypy.ini b/.mypy.ini index d3a0d2e2..75f4ccdb 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -1,5 +1,5 @@ [mypy] -follow_imports = skip +follow_imports = normal ignore_missing_imports = True scripts_are_modules = True ; Without this bare `mypy ` may get warnings for e.g. From 841ae2006ed69194a6fe284f72bf04dc51346f76 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 15:06:47 +0000 Subject: [PATCH 56/72] mypy: Add script to run against all, and use in github/push-checks --- .github/workflows/push-checks.yml | 4 ++++ scripts/mypy-all.sh | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 scripts/mypy-all.sh diff --git a/.github/workflows/push-checks.yml b/.github/workflows/push-checks.yml index e0c98897..5fdd1a14 100644 --- a/.github/workflows/push-checks.yml +++ b/.github/workflows/push-checks.yml @@ -59,3 +59,7 @@ jobs: grep -E -z -Z '\.py$' | \ xargs -0 flake8 --count --statistics --extend-ignore D ####################################################################### + + - name: mypy type checks + run: | + ./scripts/mypy-all.sh diff --git a/scripts/mypy-all.sh b/scripts/mypy-all.sh new file mode 100644 index 00000000..71f2f958 --- /dev/null +++ b/scripts/mypy-all.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# + +mypy $(git ls-tree --full-tree -r --name-only HEAD | grep -E '\.py$') From 8bacbf77ff2f4e42daaa63b9fc47dc766d06158b Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 15:11:56 +0000 Subject: [PATCH 57/72] github/push-checks: Run on any branch except main, stable, beta, releases --- .github/workflows/push-checks.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push-checks.yml b/.github/workflows/push-checks.yml index 5fdd1a14..0ebd6dee 100644 --- a/.github/workflows/push-checks.yml +++ b/.github/workflows/push-checks.yml @@ -9,7 +9,12 @@ name: Push-Checks on: push: - branches: [ develop ] + # We'll catch issues on `develop` or any PR branch. + branches-ignore: + - 'main' + - 'stable' + - 'releases' + - 'beta' jobs: build: From e70b3c99f2cd63d3e35c0b0167331b0bede33366 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 15:18:32 +0000 Subject: [PATCH 58/72] github/push-checks: Correct the job name, from `build` --- .github/workflows/push-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push-checks.yml b/.github/workflows/push-checks.yml index 0ebd6dee..b483ebb0 100644 --- a/.github/workflows/push-checks.yml +++ b/.github/workflows/push-checks.yml @@ -17,7 +17,7 @@ on: - 'beta' jobs: - build: + push_checks: runs-on: ubuntu-22.04 From 862565e95507fcdbde5f30902cf8e3381b1e4159 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 17:18:56 +0200 Subject: [PATCH 59/72] github/push-checks: set mypy-all script to be +x --- scripts/mypy-all.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/mypy-all.sh diff --git a/scripts/mypy-all.sh b/scripts/mypy-all.sh old mode 100644 new mode 100755 From 3d53b1a54d18b098e02642c83f60dbdeaa12f289 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 15:24:45 +0000 Subject: [PATCH 60/72] github/mypy: Specifically run for win32 platform --- .github/workflows/push-checks.yml | 2 +- scripts/mypy-all.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push-checks.yml b/.github/workflows/push-checks.yml index b483ebb0..07f45e75 100644 --- a/.github/workflows/push-checks.yml +++ b/.github/workflows/push-checks.yml @@ -67,4 +67,4 @@ jobs: - name: mypy type checks run: | - ./scripts/mypy-all.sh + ./scripts/mypy-all.sh --platform win32 diff --git a/scripts/mypy-all.sh b/scripts/mypy-all.sh index 71f2f958..02a76a48 100755 --- a/scripts/mypy-all.sh +++ b/scripts/mypy-all.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash # -mypy $(git ls-tree --full-tree -r --name-only HEAD | grep -E '\.py$') +mypy $@ $(git ls-tree --full-tree -r --name-only HEAD | grep -E '\.py$') From db956cd68b5ea162c40e26562aafd66534c36fe9 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Dec 2022 15:32:09 +0000 Subject: [PATCH 61/72] scripts/mypy-all: Add a little comment documentation --- scripts/mypy-all.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/mypy-all.sh b/scripts/mypy-all.sh index 02a76a48..72a37f0b 100755 --- a/scripts/mypy-all.sh +++ b/scripts/mypy-all.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash # +# Run mypy checks against all the relevant files +# We assume that all `.py` files in git should be checked, and *only* those. mypy $@ $(git ls-tree --full-tree -r --name-only HEAD | grep -E '\.py$') From 876afafcacf902f27ec2ad09cc75051e8de2f08b Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 17:49:07 +0200 Subject: [PATCH 62/72] config/linux.py Fix mypy complaints This wasn't updated to note optionals when the parent base class was --- config/linux.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/linux.py b/config/linux.py index 04087b32..3eee4c93 100644 --- a/config/linux.py +++ b/config/linux.py @@ -3,7 +3,7 @@ import os import pathlib import sys from configparser import ConfigParser -from typing import List, Optional, Union +from typing import Any, List, Optional, Union from config import AbstractConfig, appname, logger @@ -119,7 +119,7 @@ class LinuxConfig(AbstractConfig): return self.config[self.SECTION].get(key) - def get_str(self, key: str, *, default: str = None) -> str: + def get_str(self, key: str, *, default: str | None = None) -> str: """ Return the string referred to by the given key if it exists, or the default. @@ -134,7 +134,7 @@ class LinuxConfig(AbstractConfig): return self.__unescape(data) - def get_list(self, key: str, *, default: list = None) -> list: + def get_list(self, key: str, *, default: list | None = None) -> list: """ Return the list referred to by the given key if it exists, or the default. @@ -168,7 +168,7 @@ class LinuxConfig(AbstractConfig): except ValueError as e: raise ValueError(f'requested {key=} as int cannot be converted to int') from e - def get_bool(self, key: str, *, default: bool = None) -> bool: + def get_bool(self, key: str, *, default: bool | None = None) -> bool: """ Return the bool referred to by the given key if it exists, or the default. From 50dd6035201c0b837e7d1c1b9e8bf0dc9158888f Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 17:53:58 +0200 Subject: [PATCH 63/72] tests/config: Disable mypy on _old_config.py This file is just for regression testing and will never be typed correctly, which is why we no longer use it :D --- tests/config/_old_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/config/_old_config.py b/tests/config/_old_config.py index 0226bc36..211c1e72 100644 --- a/tests/config/_old_config.py +++ b/tests/config/_old_config.py @@ -1,3 +1,4 @@ +# type: ignore import numbers import sys import warnings From 5dd2287e68fe320a50c0e86ea33751f7e9f1ef12 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 17:55:23 +0200 Subject: [PATCH 64/72] build: add "dumb" platform check for mypy Mypy doesn't understand some clever version checks. This adds an otherwise un-needed assert to force mypy to ignore the file on linux --- Build-exe-and-msi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Build-exe-and-msi.py b/Build-exe-and-msi.py index adc04432..71e1eace 100644 --- a/Build-exe-and-msi.py +++ b/Build-exe-and-msi.py @@ -28,6 +28,9 @@ if sys.platform == 'win32': else: raise AssertionError(f'Unsupported platform {sys.platform}') + +# This added to make mypy happy +assert sys.platform == 'win32' ########################################################################### ########################################################################### From fa99225b95df47aead771aa32d1cea625eb2343b Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 17:56:44 +0200 Subject: [PATCH 65/72] myNotebook.py: Make platform check simpler Mypy does not appear to understand the `x in ...` format for this chech --- myNotebook.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/myNotebook.py b/myNotebook.py index 1989141f..30f95274 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -76,7 +76,8 @@ class Label(tk.Label): """Custom tk.Label class to fix some display issues.""" def __init__(self, master: Optional[ttk.Frame] = None, **kw): - if sys.platform in ['darwin', 'win32']: + # This format chosen over `sys.platform in (...)` as mypy and friends dont understand that + if sys.platform == 'darwin' or sys.platform == 'win32': kw['foreground'] = kw.pop('foreground', PAGEFG) kw['background'] = kw.pop('background', PAGEBG) else: From 470c9b2728d43dfa552050d280e661b0a4c41965 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 18:02:55 +0200 Subject: [PATCH 66/72] hotkey/linux.py: Update stub This was missing some abstract methods --- hotkey/linux.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/hotkey/linux.py b/hotkey/linux.py index 9a60515f..5074c319 100644 --- a/hotkey/linux.py +++ b/hotkey/linux.py @@ -20,6 +20,37 @@ class LinuxHotKeyMgr(AbstractHotkeyMgr): """Unregister the hotkey handling.""" pass + def acquire_start(self) -> None: + """Start acquiring hotkey state via polling.""" + pass + + def acquire_stop(self) -> None: + """Stop acquiring hotkey state.""" + pass + + def fromevent(self, event) -> bool | tuple | None: + """ + Return configuration (keycode, modifiers) or None=clear or False=retain previous. + + event.state is a pain - it shows the state of the modifiers *before* a modifier key was pressed. + event.state *does* differentiate between left and right Ctrl and Alt and between Return and Enter + by putting KF_EXTENDED in bit 18, but RegisterHotKey doesn't differentiate. + + :param event: tk event ? + :return: False to retain previous, None to not use, else (keycode, modifiers) + """ + pass + + def display(self, keycode: int, modifiers: int) -> str: + """ + Return displayable form of given hotkey + modifiers. + + :param keycode: + :param modifiers: + :return: string form + """ + return "Unsupported on linux" + def play_good(self) -> None: """Play the 'good' sound.""" pass From 490bd9dbddbea6a2d309844de74804d9a65dcbde Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 18:07:39 +0200 Subject: [PATCH 67/72] hotkeys/windows.py: Add explicit platform check --- hotkey/windows.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hotkey/windows.py b/hotkey/windows.py index 0bc54d1a..8a1c7acd 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -2,6 +2,7 @@ import atexit import ctypes import pathlib +import sys import threading import tkinter as tk import winsound @@ -12,6 +13,8 @@ from config import config from EDMCLogging import get_main_logger from hotkey import AbstractHotkeyMgr +assert sys.platform == 'win32' + logger = get_main_logger() RegisterHotKey = ctypes.windll.user32.RegisterHotKey From f94072a99e813b3928e122309d48d00c7c3dd73a Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 18:17:33 +0200 Subject: [PATCH 68/72] EDMarketConnector.py: Guard platform specific func Some callback functions are only used on windows, but defined for everyone, this causes mypy to fail in fun ways. --- EDMarketConnector.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index b6bbcf11..b9c7bdf2 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -1617,10 +1617,11 @@ class AppWindow(object): monitor.system and tk.NORMAL or tk.DISABLED) - def ontop_changed(self, event=None) -> None: - """Set main window 'on top' state as appropriate.""" - config.set('always_ontop', self.always_ontop.get()) - self.w.wm_attributes('-topmost', self.always_ontop.get()) + if sys.platform == 'win32': + def ontop_changed(self, event=None) -> None: + """Set main window 'on top' state as appropriate.""" + config.set('always_ontop', self.always_ontop.get()) + self.w.wm_attributes('-topmost', self.always_ontop.get()) def copy(self, event=None) -> None: """Copy system, and possible station, name to clipboard.""" @@ -1760,13 +1761,14 @@ class AppWindow(object): with open(f, 'wb') as h: h.write(str(companion.session.capi_raw_data).encode(encoding='utf-8')) - def exit_tray(self, systray: 'SysTrayIcon') -> None: - """Tray icon is shutting down.""" - exit_thread = threading.Thread( - target=self.onexit, - daemon=True, - ) - exit_thread.start() + if sys.platform == 'win32': + def exit_tray(self, systray: 'SysTrayIcon') -> None: + """Tray icon is shutting down.""" + exit_thread = threading.Thread( + target=self.onexit, + daemon=True, + ) + exit_thread.start() def onexit(self, event=None) -> None: """Application shutdown procedure.""" From a930f5b9024649efd7fb36a569c68683b9fe9802 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 18:25:14 +0200 Subject: [PATCH 69/72] protocol.py: Make mypy happy This adds an assert that mypy will understand, and a type ignore because it seems to ignore those asserts in imports --- protocol.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/protocol.py b/protocol.py index 290a7031..f0e1b4a8 100644 --- a/protocol.py +++ b/protocol.py @@ -121,9 +121,13 @@ elif (config.auth_force_edmc_protocol and not is_wine and not config.auth_force_localserver )): + # This could be false if you use auth_force_edmc_protocol, but then you get to keep the pieces + assert sys.platform == 'win32' # spell-checker: words HBRUSH HICON WPARAM wstring WNDCLASS HMENU HGLOBAL from ctypes import windll # type: ignore - from ctypes import POINTER, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at + from ctypes import ( # type: ignore + POINTER, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at + ) from ctypes.wintypes import ( ATOM, BOOL, DWORD, HBRUSH, HGLOBAL, HICON, HINSTANCE, HMENU, HWND, INT, LPARAM, LPCWSTR, LPVOID, LPWSTR, MSG, UINT, WPARAM From 4527177f9b753172e49c2b77a0e87cd933e428f5 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 18:33:47 +0200 Subject: [PATCH 70/72] config/linux.py: Removed List and Optional usage Missed these in the initial PR -- using the python 3.10 versions --- config/linux.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/config/linux.py b/config/linux.py index 3eee4c93..5d543d3f 100644 --- a/config/linux.py +++ b/config/linux.py @@ -3,7 +3,6 @@ import os import pathlib import sys from configparser import ConfigParser -from typing import Any, List, Optional, Union from config import AbstractConfig, appname, logger @@ -18,7 +17,7 @@ class LinuxConfig(AbstractConfig): __unescape_lut = {'\\': '\\', 'n': '\n', ';': ';', 'r': '\r', '#': '#'} __escape_lut = {'\\': '\\', '\n': 'n', ';': ';', '\r': 'r'} - def __init__(self, filename: Optional[str] = None) -> None: + def __init__(self, filename: str | None = None) -> None: super().__init__() # http://standards.freedesktop.org/basedir-spec/latest/ar01s03.html xdg_data_home = pathlib.Path(os.getenv('XDG_DATA_HOME', default='~/.local/share')).expanduser() @@ -42,7 +41,7 @@ class LinuxConfig(AbstractConfig): self.filename.parent.mkdir(exist_ok=True, parents=True) - self.config: Optional[ConfigParser] = ConfigParser(comment_prefixes=('#',), interpolation=None) + self.config: ConfigParser | None = ConfigParser(comment_prefixes=('#',), interpolation=None) self.config.read(self.filename) # read() ignores files that dont exist # Ensure that our section exists. This is here because configparser will happily create files for us, but it @@ -85,7 +84,7 @@ class LinuxConfig(AbstractConfig): :param s: str - The string to unescape. :return: str - The unescaped string. """ - out: List[str] = [] + out: list[str] = [] i = 0 while i < len(s): c = s[i] @@ -107,7 +106,7 @@ class LinuxConfig(AbstractConfig): return "".join(out) - def __raw_get(self, key: str) -> Optional[str]: + def __raw_get(self, key: str) -> str | None: """ Get a raw data value from the config file. @@ -183,7 +182,7 @@ class LinuxConfig(AbstractConfig): return bool(int(data)) - def set(self, key: str, val: Union[int, str, List[str]]) -> None: + def set(self, key: str, val: int | str | list[str]) -> None: """ Set the given key's data to the given value. @@ -192,7 +191,7 @@ class LinuxConfig(AbstractConfig): if self.config is None: raise ValueError('attempt to use a closed config') - to_set: Optional[str] = None + to_set: str | None = None if isinstance(val, bool): to_set = str(int(val)) From ca915782f678101ee0e987a2d21345c7ec99e527 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 23 Dec 2022 19:01:02 +0200 Subject: [PATCH 71/72] hotkey: add sys.platform guards to all files This both makes mypy happy and ensures that the wrong file is not imported in the wrong place --- hotkey/darwin.py | 3 ++- hotkey/linux.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/hotkey/darwin.py b/hotkey/darwin.py index a053a55f..cbf9d260 100644 --- a/hotkey/darwin.py +++ b/hotkey/darwin.py @@ -3,6 +3,7 @@ import pathlib import sys import tkinter as tk from typing import Callable, Optional, Tuple, Union +assert sys.platform == 'darwin' import objc from AppKit import ( @@ -36,7 +37,7 @@ class MacHotkeyMgr(AbstractHotkeyMgr): def __init__(self): self.MODIFIERMASK = NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask \ - | NSNumericPadKeyMask + | NSNumericPadKeyMask self.root: tk.Tk self.keycode = 0 diff --git a/hotkey/linux.py b/hotkey/linux.py index 5074c319..927c4d26 100644 --- a/hotkey/linux.py +++ b/hotkey/linux.py @@ -1,7 +1,11 @@ """Linux implementation of hotkey.AbstractHotkeyMgr.""" +import sys + from EDMCLogging import get_main_logger from hotkey import AbstractHotkeyMgr +assert sys.platform == 'linux' + logger = get_main_logger() From f2b18ed871638335570e57f49754347f484d58f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Dec 2022 17:01:02 +0000 Subject: [PATCH 72/72] build(deps-dev): bump flake8-isort from 5.0.3 to 6.0.0 Bumps [flake8-isort](https://github.com/gforcada/flake8-isort) from 5.0.3 to 6.0.0. - [Release notes](https://github.com/gforcada/flake8-isort/releases) - [Changelog](https://github.com/gforcada/flake8-isort/blob/master/CHANGES.rst) - [Commits](https://github.com/gforcada/flake8-isort/compare/5.0.3...6.0.0) --- updated-dependencies: - dependency-name: flake8-isort dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index cb25fbf8..4207743e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,7 +14,7 @@ flake8-cognitive-complexity==0.1.0 flake8-comprehensions==3.10.1 flake8-docstrings==1.6.0 isort==5.11.4 -flake8-isort==5.0.3 +flake8-isort==6.0.0 flake8-json==21.7.0 flake8-noqa==1.3.0 flake8-polyfill==1.0.2