From 37ca1c3c19a858d1388b995fd2ef5029aff5e464 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 11:32:02 +0000 Subject: [PATCH 01/41] journal_lock: Remove un-needed noqa --- journal_lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/journal_lock.py b/journal_lock.py index ef5cf983..b8882a51 100644 --- a/journal_lock.py +++ b/journal_lock.py @@ -133,7 +133,7 @@ class JournalLock: return JournalLockResult.LOCKED - def release_lock(self) -> bool: # noqa: CCR001 + def release_lock(self) -> bool: """ Release lock on journal directory. From 485a1e3bb4c790546a504860b918daae7f4fe28f Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 12:01:26 +0000 Subject: [PATCH 02/41] loadout.py: flake8 & mypy pass * Catch if an empty string (or other not-None Falsey value) is passed in. * Use pathlib for constructed filename. --- loadout.py | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/loadout.py b/loadout.py index dabce54b..805f245a 100644 --- a/loadout.py +++ b/loadout.py @@ -1,48 +1,67 @@ -# Export ship loadout in Companion API json format +"""Export ship loadout in Companion API json format.""" import json import os -from os.path import join +import pathlib import re import time +from os.path import join +from typing import Optional -from config import config import companion import util_ships +from config import config +from EDMCLogging import get_main_logger + +logger = get_main_logger() -def export(data, filename=None): +def export(data: companion.CAPIData, requested_filename: Optional[str] = None) -> None: + """ + Write Ship Loadout in Companion API JSON format. + :param data: CAPI data containing ship loadout. + :param requested_filename: Name of file to write to. + """ string = json.dumps( companion.ship(data), cls=companion.CAPIDataEncoder, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ') ) # pretty print - if filename: - with open(filename, 'wt') as h: + if requested_filename is not None and requested_filename: + with open(requested_filename, 'wt') as h: h.write(string) return + elif not requested_filename: + logger.error(f"{requested_filename=} is not valid") + return + # Look for last ship of this type ship = util_ships.ship_file_name(data['ship'].get('shipName'), data['ship']['name']) - regexp = re.compile(re.escape(ship) + '\.\d\d\d\d\-\d\d\-\d\dT\d\d\.\d\d\.\d\d\.txt') + regexp = re.compile(re.escape(ship) + r'\.\d\d\d\d\-\d\d\-\d\dT\d\d\.\d\d\.\d\d\.txt') oldfiles = sorted([x for x in os.listdir(config.get_str('outdir')) if regexp.match(x)]) if oldfiles: with open(join(config.get_str('outdir'), oldfiles[-1]), 'rU') as h: if h.read() == string: - return # same as last time - don't write + return # same as last time - don't write - querytime = config.get_int('querytime', default=int(time.time())) + query_time = config.get_int('querytime', default=int(time.time())) # Write # # When this is refactored into multi-line CHECK IT WORKS, avoiding the # brainfart we had with dangling commas in commodity.py:export() !!! # - filename = join(config.get_str('outdir'), '%s.%s.txt' % (ship, time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime)))) + output_path = pathlib.Path(config.get_str('outdir')) + output_filename = pathlib.Path( + ship + '.' + time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(query_time)) + '.txt' + ) + # Can't re-use `filename`, different type + file_name = output_path / output_filename # # When this is refactored into multi-line CHECK IT WORKS, avoiding the # brainfart we had with dangling commas in commodity.py:export() !!! # - with open(filename, 'wt') as h: + with open(file_name, 'wt') as h: h.write(string) From 84860607d72b81c274e821ef22c3fdf8266d4498 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 12:09:45 +0000 Subject: [PATCH 03/41] myNotebook.py: Add file and class docstrings --- myNotebook.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/myNotebook.py b/myNotebook.py index 85566a21..7259f93e 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -1,15 +1,19 @@ -# -# Hacks to fix various display issues with notebooks and their child widgets on OSX and Windows. -# - Windows: page background should be White, not SystemButtonFace -# - OSX: page background should be a darker gray than systemWindowBody -# selected tab foreground should be White when the window is active -# +""" +Custom `ttk.Notebook` to fix various display issues. + +Hacks to fix various display issues with notebooks and their child widgets on +OSX and Windows. + +- Windows: page background should be White, not SystemButtonFace +- OSX: page background should be a darker gray than systemWindowBody + selected tab foreground should be White when the window is active + +Entire file may be imported by plugins. +""" import sys import tkinter as tk from tkinter import ttk -# Entire file may be imported by plugins - # Can't do this with styles on OSX - http://www.tkdocs.com/tutorial/styles.html#whydifficult if sys.platform == 'darwin': from platform import mac_ver @@ -22,6 +26,7 @@ elif sys.platform == 'win32': class Notebook(ttk.Notebook): + """Custom ttk.Notebook class to fix some display issues.""" def __init__(self, master=None, **kw): @@ -47,6 +52,7 @@ class Notebook(ttk.Notebook): class Frame(sys.platform == 'darwin' and tk.Frame or ttk.Frame): + """Custom t(t)k.Frame class to fix some display issues.""" def __init__(self, master=None, **kw): if sys.platform == 'darwin': @@ -63,6 +69,7 @@ class Frame(sys.platform == 'darwin' and tk.Frame or ttk.Frame): class Label(tk.Label): + """Custom tk.Label class to fix some display issues.""" def __init__(self, master=None, **kw): if sys.platform in ['darwin', 'win32']: @@ -75,6 +82,7 @@ class Label(tk.Label): class Entry(sys.platform == 'darwin' and tk.Entry or ttk.Entry): + """Custom t(t)k.Entry class to fix some display issues.""" def __init__(self, master=None, **kw): if sys.platform == 'darwin': @@ -85,6 +93,7 @@ class Entry(sys.platform == 'darwin' and tk.Entry or ttk.Entry): class Button(sys.platform == 'darwin' and tk.Button or ttk.Button): + """Custom t(t)k.Button class to fix some display issues.""" def __init__(self, master=None, **kw): if sys.platform == 'darwin': @@ -97,6 +106,7 @@ class Button(sys.platform == 'darwin' and tk.Button or ttk.Button): class ColoredButton(sys.platform == 'darwin' and tk.Label or tk.Button): + """Custom t(t)k.ColoredButton class to fix some display issues.""" def __init__(self, master=None, **kw): if sys.platform == 'darwin': @@ -114,6 +124,7 @@ class ColoredButton(sys.platform == 'darwin' and tk.Label or tk.Button): class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton): + """Custom t(t)k.Checkbutton class to fix some display issues.""" def __init__(self, master=None, **kw): if sys.platform == 'darwin': @@ -127,6 +138,7 @@ class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton class Radiobutton(sys.platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton): + """Custom t(t)k.Radiobutton class to fix some display issues.""" def __init__(self, master=None, **kw): if sys.platform == 'darwin': @@ -140,6 +152,7 @@ class Radiobutton(sys.platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton class OptionMenu(sys.platform == 'darwin' and tk.OptionMenu or ttk.OptionMenu): + """Custom t(t)k.OptionMenu class to fix some display issues.""" def __init__(self, master, variable, default=None, *values, **kw): if sys.platform == 'darwin': From 514f3fac8a85d5d543abfaed9af7181b1a4c89c9 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 12:16:44 +0000 Subject: [PATCH 04/41] myNotebook.py: Now passes flake8 & mypy * Typed `master` to each `__init__()`. * Having to `# type: ignore` the base class for the classes which are "tk on darwin, else ttk" as this 'dynamic' type confuses mypy. See the comment on `class Frame` for a suggested proper fix, which will be more work. --- myNotebook.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/myNotebook.py b/myNotebook.py index 7259f93e..eb17bf2f 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -13,6 +13,7 @@ Entire file may be imported by plugins. import sys import tkinter as tk from tkinter import ttk +from typing import Optional # Can't do this with styles on OSX - http://www.tkdocs.com/tutorial/styles.html#whydifficult if sys.platform == 'darwin': @@ -28,7 +29,7 @@ elif sys.platform == 'win32': class Notebook(ttk.Notebook): """Custom ttk.Notebook class to fix some display issues.""" - def __init__(self, master=None, **kw): + def __init__(self, master: Optional[ttk.Frame] = None, **kw): ttk.Notebook.__init__(self, master, **kw) style = ttk.Style() @@ -51,10 +52,13 @@ class Notebook(ttk.Notebook): self.grid(padx=10, pady=10, sticky=tk.NSEW) -class Frame(sys.platform == 'darwin' and tk.Frame or ttk.Frame): +# FIXME: The real fix for this 'dynamic type' would be to split this whole +# thing into being a module with per-platform files, as we've done with config +# That would also make the code cleaner. +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=None, **kw): + def __init__(self, master: Optional[ttk.Frame] = None, **kw): if sys.platform == 'darwin': kw['background'] = kw.pop('background', PAGEBG) tk.Frame.__init__(self, master, **kw) @@ -71,7 +75,7 @@ class Frame(sys.platform == 'darwin' and tk.Frame or ttk.Frame): class Label(tk.Label): """Custom tk.Label class to fix some display issues.""" - def __init__(self, master=None, **kw): + def __init__(self, master: Optional[ttk.Frame] = None, **kw): if sys.platform in ['darwin', 'win32']: kw['foreground'] = kw.pop('foreground', PAGEFG) kw['background'] = kw.pop('background', PAGEBG) @@ -81,10 +85,10 @@ class Label(tk.Label): tk.Label.__init__(self, master, **kw) # Just use tk.Label on all platforms -class Entry(sys.platform == 'darwin' and tk.Entry or ttk.Entry): +class Entry(sys.platform == 'darwin' and tk.Entry or ttk.Entry): # type: ignore """Custom t(t)k.Entry class to fix some display issues.""" - def __init__(self, master=None, **kw): + def __init__(self, master: Optional[ttk.Frame] = None, **kw): if sys.platform == 'darwin': kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG) tk.Entry.__init__(self, master, **kw) @@ -92,10 +96,10 @@ class Entry(sys.platform == 'darwin' and tk.Entry or ttk.Entry): ttk.Entry.__init__(self, master, **kw) -class Button(sys.platform == 'darwin' and tk.Button or ttk.Button): +class Button(sys.platform == 'darwin' and tk.Button or ttk.Button): # type: ignore """Custom t(t)k.Button class to fix some display issues.""" - def __init__(self, master=None, **kw): + def __init__(self, master: Optional[ttk.Frame] = None, **kw): if sys.platform == 'darwin': kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG) tk.Button.__init__(self, master, **kw) @@ -105,10 +109,10 @@ class Button(sys.platform == 'darwin' and tk.Button or ttk.Button): ttk.Button.__init__(self, master, **kw) -class ColoredButton(sys.platform == 'darwin' and tk.Label or tk.Button): +class ColoredButton(sys.platform == 'darwin' and tk.Label or tk.Button): # type: ignore """Custom t(t)k.ColoredButton class to fix some display issues.""" - def __init__(self, master=None, **kw): + def __init__(self, master: Optional[ttk.Frame] = None, **kw): if sys.platform == 'darwin': # Can't set Button background on OSX, so use a Label instead kw['relief'] = kw.pop('relief', tk.RAISED) @@ -123,10 +127,10 @@ class ColoredButton(sys.platform == 'darwin' and tk.Label or tk.Button): self._command() -class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton): +class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton): # type: ignore """Custom t(t)k.Checkbutton class to fix some display issues.""" - def __init__(self, master=None, **kw): + def __init__(self, master: Optional[ttk.Frame] = None, **kw): if sys.platform == 'darwin': kw['foreground'] = kw.pop('foreground', PAGEFG) kw['background'] = kw.pop('background', PAGEBG) @@ -137,10 +141,10 @@ class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton ttk.Checkbutton.__init__(self, master, **kw) -class Radiobutton(sys.platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton): +class Radiobutton(sys.platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton): # type: ignore """Custom t(t)k.Radiobutton class to fix some display issues.""" - def __init__(self, master=None, **kw): + def __init__(self, master: Optional[ttk.Frame] = None, **kw): if sys.platform == 'darwin': kw['foreground'] = kw.pop('foreground', PAGEFG) kw['background'] = kw.pop('background', PAGEBG) @@ -151,7 +155,7 @@ class Radiobutton(sys.platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton ttk.Radiobutton.__init__(self, master, **kw) -class OptionMenu(sys.platform == 'darwin' and tk.OptionMenu or ttk.OptionMenu): +class OptionMenu(sys.platform == 'darwin' and tk.OptionMenu or ttk.OptionMenu): # type: ignore """Custom t(t)k.OptionMenu class to fix some display issues.""" def __init__(self, master, variable, default=None, *values, **kw): From 8628efa0a1437534292ea796dc6d6806395e520a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 12:40:33 +0000 Subject: [PATCH 05/41] github/pr-/push-checks: Attempt non-diff flake8 checks * flake8 6.0.0 dropped support for, the broken, --diff. * We want to only run against python files. We will have 'git diff's for other types of files. * Uses 'git diff -z', 'grep -z -Z' and 'xargs -0' so as to pass NUL-terminated strings around to avoid "special characters in path/filenames" issues. --- .github/workflows/pr-checks.yml | 8 ++++++-- .github/workflows/push-checks.yml | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index e90929bb..70c8dad1 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -85,11 +85,15 @@ jobs: # F63 - 'tests' checking # F7 - syntax errors # F82 - undefined checking - git diff "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --diff + echo git diff --name-only --diff-filter=d -z "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | \ + grep -E -z -Z '\.py$' | \ + xargs -0 flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # Can optionally add `--exit-zero` to the flake8 arguments so that # this doesn't fail the build. # explicitly ignore docstring errors (start with D) - git diff "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | flake8 . --count --statistics --diff --extend-ignore D + git diff --name-only --diff-filter=d -z "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | \ + grep -E -z -Z '\.py$' | \ + xargs -0 flake8 . --count --statistics --extend-ignore D #################################################################### #################################################################### diff --git a/.github/workflows/push-checks.yml b/.github/workflows/push-checks.yml index b6994b02..a11234ed 100644 --- a/.github/workflows/push-checks.yml +++ b/.github/workflows/push-checks.yml @@ -40,7 +40,20 @@ jobs: DATA=$(jq --raw-output .before $GITHUB_EVENT_PATH) echo "DATA: ${DATA}" + ####################################################################### # stop the build if there are Python syntax errors or undefined names, ignore existing - git diff "$DATA" | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --diff + ####################################################################### + # We need to get just the *filenames* of only *python* files changed. + # Using various -z/-Z/-0 to utilise NUL-terminated strings. + git diff --name-only --diff-filter=d -z "$DATA" | \ + grep -E -z -Z '\.py$' | \ + xargs -0 flake8 --count --select=E9,F63,F7,F82 --show-source --statistics + ####################################################################### + + ####################################################################### # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - git diff "$DATA" | flake8 . --count --statistics --diff + ####################################################################### + git diff --name-only --diff-filter=d -z "$DATA" | \ + grep -E -z -Z '\.py$' | \ + xargs -0 flake8 . --count --statistics + ####################################################################### From 413b2f06f88ef30a3b956b8e148dd08dc145abbb Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 14:03:47 +0000 Subject: [PATCH 06/41] plug.py: Some docstrings, change to plugin loading * Plugin loading: Avoid using .format() --- plug.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/plug.py b/plug.py index 2f3ab888..7abe7c49 100644 --- a/plug.py +++ b/plug.py @@ -1,6 +1,4 @@ -""" -Plugin hooks for EDMC - Ian Norton, Jonathan Harris -""" +"""Plugin API.""" import copy import importlib import logging @@ -29,15 +27,17 @@ last_error = { class Plugin(object): + """An EDMC plugin.""" def __init__(self, name: str, loadfile: str, plugin_logger: Optional[logging.Logger]): """ - Load a single plugin - :param name: module name - :param loadfile: the main .py file + Load a single plugin. + + :param name: Base name of the file being loaded from. + :param loadfile: Full path/filename of the plugin. + :param plugin_logger: The logging instance for this plugin to use. :raises Exception: Typically ImportError or OSError """ - self.name = name # Display name. self.folder = name # basename of plugin folder. None for internal plugins. self.module = None # None for disabled plugins. @@ -46,9 +46,13 @@ class Plugin(object): if loadfile: logger.info(f'loading plugin "{name.replace(".", "_")}" from "{loadfile}"') try: - module = importlib.machinery.SourceFileLoader('plugin_{}'.format( - name.encode(encoding='ascii', errors='replace').decode('utf-8').replace('.', '_')), - loadfile).load_module() + filename = 'plugin_' + filename += name.encode(encoding='ascii', errors='replace').decode('utf-8').replace('.', '_') + module = importlib.machinery.SourceFileLoader( + filename, + loadfile + ).load_module() + if getattr(module, 'plugin_start3', None): newname = module.plugin_start3(os.path.dirname(loadfile)) self.name = newname and str(newname) or name From 3247fb805ca731c8c5a7e3aff87a6b273d5921a4 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 14:05:59 +0000 Subject: [PATCH 07/41] plug.py: Further docstring fixes --- plug.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/plug.py b/plug.py index 7abe7c49..08c853b5 100644 --- a/plug.py +++ b/plug.py @@ -62,7 +62,7 @@ class Plugin(object): PLUGINS_not_py3.append(self) else: logger.error(f'plugin {name} has no plugin_start3() function') - except Exception as e: + except Exception: logger.exception(f': Failed for Plugin "{name}"') raise else: @@ -70,7 +70,8 @@ class Plugin(object): def _get_func(self, funcname): """ - Get a function from a plugin + Get a function from a plugin. + :param funcname: :returns: The function, or None if it isn't implemented. """ @@ -79,6 +80,7 @@ class Plugin(object): def get_app(self, parent): """ If the plugin provides mainwindow content create and return it. + :param parent: the parent frame for this entry. :returns: None, a tk Widget, or a pair of tk.Widgets """ @@ -88,14 +90,23 @@ class Plugin(object): appitem = plugin_app(parent) if appitem is None: return None + elif isinstance(appitem, tuple): - if len(appitem) != 2 or not isinstance(appitem[0], tk.Widget) or not isinstance(appitem[1], tk.Widget): + if ( + len(appitem) != 2 + or not isinstance(appitem[0], tk.Widget) + or not isinstance(appitem[1], tk.Widget) + ): raise AssertionError + elif not isinstance(appitem, tk.Widget): raise AssertionError + return appitem + except Exception as e: logger.exception(f'Failed for Plugin "{self.name}"') + return None def get_prefs(self, parent, cmdr, is_beta): From 1f21c7fae430e8a0d081693133a6d80d0719c602 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 14:10:09 +0000 Subject: [PATCH 08/41] plug.py: Only type annotation coverage to go in flake8 --- plug.py | 57 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/plug.py b/plug.py index 08c853b5..e9e6ee73 100644 --- a/plug.py +++ b/plug.py @@ -90,7 +90,7 @@ class Plugin(object): appitem = plugin_app(parent) if appitem is None: return None - + elif isinstance(appitem, tuple): if ( len(appitem) != 2 @@ -104,7 +104,7 @@ class Plugin(object): return appitem - except Exception as e: + except Exception: logger.exception(f'Failed for Plugin "{self.name}"') return None @@ -112,6 +112,7 @@ class Plugin(object): def get_prefs(self, parent, cmdr, is_beta): """ If the plugin provides a prefs frame, create and return it. + :param parent: the parent frame for this preference tab. :param cmdr: current Cmdr name (or None). Relevant if you want to have different settings for different user accounts. @@ -125,15 +126,13 @@ class Plugin(object): if not isinstance(frame, nb.Frame): raise AssertionError return frame - except Exception as e: + except Exception: logger.exception(f'Failed for Plugin "{self.name}"') return None -def load_plugins(master): - """ - Find and load all plugins - """ +def load_plugins(master): # noqa: CCR001 + """Find and load all plugins.""" last_error['root'] = master internal = [] @@ -143,7 +142,7 @@ def load_plugins(master): plugin = Plugin(name[:-3], os.path.join(config.internal_plugin_dir_path, name), logger) plugin.folder = None # Suppress listing in Plugins prefs tab internal.append(plugin) - except Exception as e: + except Exception: logger.exception(f'Failure loading internal Plugin "{name}"') PLUGINS.extend(sorted(internal, key=lambda p: operator.attrgetter('name')(p).lower())) @@ -152,8 +151,10 @@ def load_plugins(master): found = [] # Load any plugins that are also packages first - for name in sorted(os.listdir(config.plugin_dir_path), - key=lambda n: (not os.path.isfile(os.path.join(config.plugin_dir_path, n, '__init__.py')), n.lower())): + for name in sorted( + os.listdir(config.plugin_dir_path), + key=lambda n: (not os.path.isfile(os.path.join(config.plugin_dir_path, n, '__init__.py')), n.lower()) + ): if not os.path.isdir(os.path.join(config.plugin_dir_path, name)) or name[0] in ['.', '_']: pass elif name.endswith('.disabled'): @@ -170,7 +171,7 @@ def load_plugins(master): plugin_logger = EDMCLogging.get_plugin_logger(name) found.append(Plugin(name, os.path.join(config.plugin_dir_path, name, 'load.py'), plugin_logger)) - except Exception as e: + except Exception: logger.exception(f'Failure loading found Plugin "{name}"') pass PLUGINS.extend(sorted(found, key=lambda p: operator.attrgetter('name')(p).lower())) @@ -178,7 +179,8 @@ def load_plugins(master): def provides(fn_name): """ - Find plugins that provide a function + Find plugins that provide a function. + :param fn_name: :returns: list of names of plugins that provide this function .. versionadded:: 3.0.2 @@ -188,7 +190,8 @@ def provides(fn_name): def invoke(plugin_name, fallback, fn_name, *args): """ - Invoke a function on a named plugin + Invoke a function on a named plugin. + :param plugin_name: preferred plugin on which to invoke the function :param fallback: fallback plugin on which to invoke the function, or None :param fn_name: @@ -208,6 +211,7 @@ def invoke(plugin_name, fallback, fn_name, *args): def notify_stop(): """ Notify each plugin that the program is closing. + If your plugin uses threads then stop and join() them before returning. .. versionadded:: 2.3.7 """ @@ -219,7 +223,7 @@ def notify_stop(): logger.info(f'Asking plugin "{plugin.name}" to stop...') newerror = plugin_stop() error = error or newerror - except Exception as e: + except Exception: logger.exception(f'Plugin "{plugin.name}" failed') logger.info('Done') @@ -229,7 +233,8 @@ def notify_stop(): def notify_prefs_cmdr_changed(cmdr, is_beta): """ - Notify each plugin that the Cmdr has been changed while the settings dialog is open. + Notify plugins that the Cmdr was changed while the settings dialog is open. + Relevant if you want to have different settings for different user accounts. :param cmdr: current Cmdr name (or None). :param is_beta: whether the player is in a Beta universe. @@ -239,13 +244,14 @@ def notify_prefs_cmdr_changed(cmdr, is_beta): if prefs_cmdr_changed: try: prefs_cmdr_changed(cmdr, is_beta) - except Exception as e: + except Exception: logger.exception(f'Plugin "{plugin.name}" failed') def notify_prefs_changed(cmdr, is_beta): """ - Notify each plugin that the settings dialog has been closed. + Notify plugins that the settings dialog has been closed. + The prefs frame and any widgets you created in your `get_prefs()` callback will be destroyed on return from this function, so take a copy of any values that you want to save. @@ -257,13 +263,14 @@ def notify_prefs_changed(cmdr, is_beta): if prefs_changed: try: prefs_changed(cmdr, is_beta) - except Exception as e: + except Exception: logger.exception(f'Plugin "{plugin.name}" failed') def notify_journal_entry(cmdr, is_beta, system, station, entry, state): """ Send a journal entry to each plugin. + :param cmdr: The Cmdr name, or None if not yet known :param system: The current system, or None if not yet known :param station: The current station, or None if not docked or not yet known @@ -283,21 +290,21 @@ def notify_journal_entry(cmdr, is_beta, system, station, entry, state): # Pass a copy of the journal entry in case the callee modifies it newerror = journal_entry(cmdr, is_beta, system, station, dict(entry), dict(state)) error = error or newerror - except Exception as e: + except Exception: logger.exception(f'Plugin "{plugin.name}" failed') return error def notify_journal_entry_cqc(cmdr, is_beta, entry, state): """ - Send a journal entry to each plugin. + Send an in-CQC journal entry to each plugin. + :param cmdr: The Cmdr name, or None if not yet known :param entry: The journal entry as a dictionary :param state: A dictionary containing info about the Cmdr, current ship and cargo :param is_beta: whether the player is in a Beta universe. :returns: Error message from the first plugin that returns one (if any) """ - error = None for plugin in PLUGINS: cqc_callback = plugin._get_func('journal_entry_cqc') @@ -316,6 +323,7 @@ def notify_journal_entry_cqc(cmdr, is_beta, entry, state): def notify_dashboard_entry(cmdr, is_beta, entry): """ Send a status entry to each plugin. + :param cmdr: The piloting Cmdr name :param is_beta: whether the player is in a Beta universe. :param entry: The status entry as a dictionary @@ -329,14 +337,15 @@ def notify_dashboard_entry(cmdr, is_beta, entry): # Pass a copy of the status entry in case the callee modifies it newerror = status(cmdr, is_beta, dict(entry)) error = error or newerror - except Exception as e: + except Exception: logger.exception(f'Plugin "{plugin.name}" failed') return error def notify_newdata(data, is_beta): """ - Send the latest EDMC data from the FD servers to each plugin + Send the latest EDMC data from the FD servers to each plugin. + :param data: :param is_beta: whether the player is in a Beta universe. :returns: Error message from the first plugin that returns one (if any) @@ -348,7 +357,7 @@ def notify_newdata(data, is_beta): try: newerror = cmdr_data(data, is_beta) error = error or newerror - except Exception as e: + except Exception: logger.exception(f'Plugin "{plugin.name}" failed') return error From a0c73a5c710da1299abffbb452f4281651060e89 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 14:48:50 +0000 Subject: [PATCH 09/41] plug.py: flake8 & mypy pass --- plug.py | 93 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/plug.py b/plug.py index e9e6ee73..b1c0d3da 100644 --- a/plug.py +++ b/plug.py @@ -7,8 +7,9 @@ import os import sys import tkinter as tk from builtins import object, str -from typing import Optional +from typing import Any, Callable, List, Mapping, MutableMapping, Optional, Tuple +import companion import myNotebook as nb # noqa: N813 from config import config from EDMCLogging import get_main_logger @@ -19,17 +20,25 @@ logger = get_main_logger() PLUGINS = [] PLUGINS_not_py3 = [] + # For asynchronous error display -last_error = { - 'msg': None, - 'root': None, -} +class LastError: + """Holds the last plugin error.""" + + msg: Optional[str] + root: tk.Frame + + def __init__(self) -> None: + self.msg = None + + +last_error = LastError() class Plugin(object): """An EDMC plugin.""" - def __init__(self, name: str, loadfile: str, plugin_logger: Optional[logging.Logger]): + def __init__(self, name: str, loadfile: Optional[str], plugin_logger: Optional[logging.Logger]): """ Load a single plugin. @@ -38,10 +47,10 @@ class Plugin(object): :param plugin_logger: The logging instance for this plugin to use. :raises Exception: Typically ImportError or OSError """ - self.name = name # Display name. - self.folder = name # basename of plugin folder. None for internal plugins. + self.name: str = name # Display name. + self.folder: Optional[str] = name # basename of plugin folder. None for internal plugins. self.module = None # None for disabled plugins. - self.logger = plugin_logger + self.logger: Optional[logging.Logger] = plugin_logger if loadfile: logger.info(f'loading plugin "{name.replace(".", "_")}" from "{loadfile}"') @@ -68,7 +77,7 @@ class Plugin(object): else: logger.info(f'plugin {name} disabled') - def _get_func(self, funcname): + def _get_func(self, funcname: str) -> Optional[Callable]: """ Get a function from a plugin. @@ -77,7 +86,7 @@ class Plugin(object): """ return getattr(self.module, funcname, None) - def get_app(self, parent): + def get_app(self, parent: tk.Frame) -> Optional[tk.Frame]: """ If the plugin provides mainwindow content create and return it. @@ -109,7 +118,7 @@ class Plugin(object): return None - def get_prefs(self, parent, cmdr, is_beta): + def get_prefs(self, parent: tk.Frame, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: """ If the plugin provides a prefs frame, create and return it. @@ -131,9 +140,9 @@ class Plugin(object): return None -def load_plugins(master): # noqa: CCR001 +def load_plugins(master: tk.Frame) -> None: # noqa: CCR001 """Find and load all plugins.""" - last_error['root'] = master + last_error.root = master internal = [] for name in sorted(os.listdir(config.internal_plugin_dir_path)): @@ -177,7 +186,7 @@ def load_plugins(master): # noqa: CCR001 PLUGINS.extend(sorted(found, key=lambda p: operator.attrgetter('name')(p).lower())) -def provides(fn_name): +def provides(fn_name: str) -> List[str]: """ Find plugins that provide a function. @@ -188,7 +197,9 @@ def provides(fn_name): return [p.name for p in PLUGINS if p._get_func(fn_name)] -def invoke(plugin_name, fallback, fn_name, *args): +def invoke( + plugin_name: str, fallback: str, fn_name: str, *args: Tuple +) -> Optional[str]: """ Invoke a function on a named plugin. @@ -200,15 +211,21 @@ def invoke(plugin_name, fallback, fn_name, *args): .. versionadded:: 3.0.2 """ for plugin in PLUGINS: - if plugin.name == plugin_name and plugin._get_func(fn_name): - return plugin._get_func(fn_name)(*args) + if plugin.name == plugin_name: + plugin_func = plugin._get_func(fn_name) + if plugin_func is not None: + return plugin_func(*args) + for plugin in PLUGINS: if plugin.name == fallback: - assert plugin._get_func(fn_name), plugin.name # fallback plugin should provide the function - return plugin._get_func(fn_name)(*args) + plugin_func = plugin._get_func(fn_name) + assert plugin_func, plugin.name # fallback plugin should provide the function + return plugin_func(*args) + + return None -def notify_stop(): +def notify_stop() -> Optional[str]: """ Notify each plugin that the program is closing. @@ -231,7 +248,7 @@ def notify_stop(): return error -def notify_prefs_cmdr_changed(cmdr, is_beta): +def notify_prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: """ Notify plugins that the Cmdr was changed while the settings dialog is open. @@ -248,7 +265,7 @@ def notify_prefs_cmdr_changed(cmdr, is_beta): logger.exception(f'Plugin "{plugin.name}" failed') -def notify_prefs_changed(cmdr, is_beta): +def notify_prefs_changed(cmdr: str, is_beta: bool) -> None: """ Notify plugins that the settings dialog has been closed. @@ -267,7 +284,11 @@ def notify_prefs_changed(cmdr, is_beta): logger.exception(f'Plugin "{plugin.name}" failed') -def notify_journal_entry(cmdr, is_beta, system, station, entry, state): +def notify_journal_entry( + cmdr: str, is_beta: bool, system: str, station: str, + entry: MutableMapping[str, Any], + state: Mapping[str, Any] +) -> Optional[str]: """ Send a journal entry to each plugin. @@ -295,7 +316,11 @@ def notify_journal_entry(cmdr, is_beta, system, station, entry, state): return error -def notify_journal_entry_cqc(cmdr, is_beta, entry, state): +def notify_journal_entry_cqc( + cmdr: str, is_beta: bool, + entry: MutableMapping[str, Any], + state: Mapping[str, Any] +) -> Optional[str]: """ Send an in-CQC journal entry to each plugin. @@ -320,7 +345,10 @@ def notify_journal_entry_cqc(cmdr, is_beta, entry, state): return error -def notify_dashboard_entry(cmdr, is_beta, entry): +def notify_dashboard_entry( + cmdr: str, is_beta: bool, + entry: MutableMapping[str, Any], +) -> Optional[str]: """ Send a status entry to each plugin. @@ -342,7 +370,10 @@ def notify_dashboard_entry(cmdr, is_beta, entry): return error -def notify_newdata(data, is_beta): +def notify_newdata( + data: companion.CAPIData, + is_beta: bool +) -> Optional[str]: """ Send the latest EDMC data from the FD servers to each plugin. @@ -362,7 +393,7 @@ def notify_newdata(data, is_beta): return error -def show_error(err): +def show_error(err: str) -> None: """ Display an error message in the status line of the main window. @@ -374,6 +405,6 @@ def show_error(err): logger.info(f'Called during shutdown: "{str(err)}"') return - if err and last_error['root']: - last_error['msg'] = str(err) - last_error['root'].event_generate('<>', when="tail") + if err and last_error.root: + last_error.msg = str(err) + last_error.root.event_generate('<>', when="tail") From bb2bf536476549f4ab99daa984bf4c13763b95fc Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 14:51:54 +0000 Subject: [PATCH 10/41] protocol.py: flake8 pass This is flake8 objecting to the "initialised later" `protocolhandler`. --- protocol.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/protocol.py b/protocol.py index 6db5d5be..27391498 100644 --- a/protocol.py +++ b/protocol.py @@ -107,11 +107,12 @@ if sys.platform == 'darwin' and getattr(sys, 'frozen', False): # noqa: C901 # i def handleEvent_withReplyEvent_(self, event, replyEvent) -> None: # noqa: N802 N803 # Required to override """Actual event handling from NSAppleEventManager.""" - protocolhandler.lasturl = urllib.parse.unquote( # type: ignore # Its going to be a DPH in this code + protocolhandler.lasturl = urllib.parse.unquote( # noqa: F821: type: ignore # Its going to be a DPH in + # this code event.paramDescriptorForKeyword_(keyDirectObject).stringValue() ).strip() - protocolhandler.master.after(DarwinProtocolHandler.POLL, protocolhandler.poll) # type: ignore + protocolhandler.master.after(DarwinProtocolHandler.POLL, protocolhandler.poll) # noqa: F821: type: ignore elif (config.auth_force_edmc_protocol @@ -389,7 +390,7 @@ else: # Linux / Run from source url = urllib.parse.unquote(self.path) if url.startswith('/auth'): logger.debug('Request starts with /auth, sending to protocolhandler.event()') - protocolhandler.event(url) + protocolhandler.event(url) # noqa: F821 self.send_response(200) return True else: From 0d2505ea48e41974d2013bbae4a60da8120d73bf Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 15:13:59 +0000 Subject: [PATCH 11/41] protocol.py: mypy pass * 'type: ignore' some ctypes operations on variables. * Use `c_long()` on some returns. The ctypes types do work that way as constructors. * Fix the BAseHTTPHandler.log_request() types to match superclass. --- protocol.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/protocol.py b/protocol.py index 27391498..290a7031 100644 --- a/protocol.py +++ b/protocol.py @@ -1,5 +1,4 @@ """protocol handler for cAPI authorisation.""" - # spell-checker: words ntdll GURL alloc wfile instantiatable pyright import os import sys @@ -35,7 +34,7 @@ class GenericProtocolHandler: def __init__(self) -> None: self.redirect = protocolhandler_redirect # Base redirection URL self.master: 'tkinter.Tk' = None # type: ignore - self.lastpayload = None + self.lastpayload: Optional[str] = None def start(self, master: 'tkinter.Tk') -> None: """Start Protocol Handler.""" @@ -45,7 +44,7 @@ class GenericProtocolHandler: """Stop / Close Protocol Handler.""" pass - def event(self, url) -> None: + def event(self, url: str) -> None: """Generate an auth event.""" self.lastpayload = url @@ -197,7 +196,7 @@ elif (config.auth_force_edmc_protocol # Windows Message handler stuff (IPC) # https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms633573(v=vs.85) @WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM) - def WndProc(hwnd: HWND, message: UINT, wParam, lParam): # noqa: N803 N802 + def WndProc(hwnd: HWND, message: UINT, wParam: WPARAM, lParam: LPARAM) -> c_long: # noqa: N803 N802 """ Deal with DDE requests. @@ -216,8 +215,10 @@ elif (config.auth_force_edmc_protocol topic = create_unicode_buffer(256) # Note that lParam is 32 bits, and broken into two 16 bit words. This will break on 64bit as the math is # wrong - lparam_low = lParam & 0xFFFF # if nonzero, the target application for which a conversation is requested - lparam_high = lParam >> 16 # if nonzero, the topic of said conversation + # if nonzero, the target application for which a conversation is requested + lparam_low = lParam & 0xFFFF # type: ignore + # if nonzero, the topic of said conversation + lparam_high = lParam >> 16 # type: ignore # if either of the words are nonzero, they contain # atoms https://docs.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables @@ -237,7 +238,10 @@ elif (config.auth_force_edmc_protocol wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, GlobalAddAtomW(appname), GlobalAddAtomW('System')) ) - return 0 + # It works as a constructor as per + return c_long(0) + + return c_long(1) # This is an utter guess -Ath class WindowsProtocolHandler(GenericProtocolHandler): """ @@ -350,7 +354,7 @@ else: # Linux / Run from source self.thread: Optional[threading.Thread] = None - def start(self, master) -> None: + def start(self, master: 'tkinter.Tk') -> None: """Start the HTTP server thread.""" GenericProtocolHandler.start(self, master) self.thread = threading.Thread(target=self.worker, name='OAuth worker') @@ -412,7 +416,7 @@ else: # Linux / Run from source else: self.end_headers() - def log_request(self, code, size=None): + def log_request(self, code: int | str = '-', size: int | str = '-') -> None: """Override to prevent logging.""" pass From 5edde547fae17b4d40c76fb0d2b5cf6600435088 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 15:43:33 +0000 Subject: [PATCH 12/41] shipyard.py: flake8/mypy & conver to use `csv` module --- shipyard.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/shipyard.py b/shipyard.py index 78961ad7..a5234c0a 100644 --- a/shipyard.py +++ b/shipyard.py @@ -1,24 +1,35 @@ -# Export list of ships as CSV +"""Export list of ships as CSV.""" +import csv -import time - -from config import config +import companion from edmc_data import ship_name_map -def export(data, filename): - - querytime = config.get_int('querytime', default=int(time.time())) +def export(data: companion.CAPIData, filename: str) -> None: + """ + Write shipyard data in Companion API JSON format. + :param data: The CAPI data. + :param filename: Optional filename to write to. + :return: + """ assert data['lastSystem'].get('name') assert data['lastStarport'].get('name') assert data['lastStarport'].get('ships') - header = 'System,Station,Ship,FDevID,Date\n' - rowheader = '%s,%s' % (data['lastSystem']['name'], data['lastStarport']['name']) + with open(filename, 'w', newline='') as f: + c = csv.writer(f) + c.writerow(('System', 'Station', 'Ship', 'FDevID', 'Date')) - h = open(filename, 'wt') - h.write(header) - for (name,fdevid) in [(ship_name_map.get(ship['name'].lower(), ship['name']), ship['id']) for ship in list((data['lastStarport']['ships'].get('shipyard_list') or {}).values()) + data['lastStarport']['ships'].get('unavailable_list')]: - h.write('%s,%s,%s,%s\n' % (rowheader, name, fdevid, data['timestamp'])) - h.close() + for (name, fdevid) in [ + ( + ship_name_map.get(ship['name'].lower(), ship['name']), + ship['id'] + ) for ship in list( + (data['lastStarport']['ships'].get('shipyard_list') or {}).values() + ) + data['lastStarport']['ships'].get('unavailable_list') + ]: + c.writerow(( + data['lastSystem']['name'], data['lastStarport']['name'], + name, fdevid, data['timestamp'] + )) From ca98549f682797fc8a7f9c0ceb9af7ce72563fc4 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 16:13:01 +0000 Subject: [PATCH 13/41] td: A start on flake8 passing To avoid overly long lines this is splitting things into separate `h.write()` calls, and making temporary variables for even some of those. --- td.py | 101 ++++++++++++++++++++++++++-------------------------------- 1 file changed, 46 insertions(+), 55 deletions(-) diff --git a/td.py b/td.py index 2f91b4a2..f75ddf93 100644 --- a/td.py +++ b/td.py @@ -1,70 +1,61 @@ -# Export to Trade Dangerous +"""Export data for Trade Dangerous.""" +import pathlib import time from collections import defaultdict from operator import itemgetter -from os.path import join from platform import system from sys import platform +from companion import CAPIData from config import applongname, appversion, config # These are specific to Trade Dangerous, so don't move to edmc_data.py -demandbracketmap = { 0: '?', - 1: 'L', - 2: 'M', - 3: 'H', } -stockbracketmap = { 0: '-', - 1: 'L', - 2: 'M', - 3: 'H', } +demandbracketmap = {0: '?', + 1: 'L', + 2: 'M', + 3: 'H', } +stockbracketmap = {0: '-', + 1: 'L', + 2: 'M', + 3: 'H', } -def export(data): +def export(data: CAPIData) -> None: + """Export market data in TD format.""" + data_path = pathlib.Path(config.get_str('outdir')) + timestamp = time.strftime('%Y-%m-%dT%H.%M.%S', time.strptime(data['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) + data_filename = f"{data['lastSystem']['name'].strip()}.{data['lastStarport']['name'].strip()}.{timestamp}.prices" - querytime = config.get_int('querytime', default=int(time.time())) + # codecs can't automatically handle line endings, so encode manually where + # required + with open(data_path / data_filename, 'wb') as h: + # Format described here: https://bitbucket.org/kfsone/tradedangerous/wiki/Price%20Data + h.write('#! trade.py import -\n'.encode('utf-8')) + this_platform = 'darwin' and "Mac OS" or system_name() + cmdr_name = data['commander']['name'].strip() + h.write(f'# Created by {applongname} {appversion()} on {this_platform} for Cmdr {cmdr_name}.\n'.encode('utf-8')) + h.write('#\n# \n\n'.encode('utf-8')) + system_name = data['lastSystem']['name'].strip() + starport_name = data['lastStarport']['name'].strip() + h.write(f'@ {system_name}/{starport_name}\n'.encode('utf-8')) - # - # When this is refactored into multi-line CHECK IT WORKS, avoiding the - # brainfart we had with dangling commas in commodity.py:export() !!! - # - filename = join(config.get_str('outdir'), '%s.%s.%s.prices' % (data['lastSystem']['name'].strip(), data['lastStarport']['name'].strip(), time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime)))) - # - # When this is refactored into multi-line CHECK IT WORKS, avoiding the - # brainfart we had with dangling commas in commodity.py:export() !!! - # + # sort commodities by category + bycategory = defaultdict(list) + for commodity in data['lastStarport']['commodities']: + bycategory[commodity['categoryname']].append(commodity) - timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.strptime(data['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) + timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.strptime(data['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) + for category in sorted(bycategory): + h.write(' + {}\n'.format(category).encode('utf-8')) + # corrections to commodity names can change the sort order + for commodity in sorted(bycategory[category], key=itemgetter('name')): + h.write(' {:<23} {:7d} {:7d} {:9}{:1} {:8}{:1} {}\n'.format( + commodity['name'], + int(commodity['sellPrice']), + int(commodity['buyPrice']), + int(commodity['demand']) if commodity['demandBracket'] else '', + demandbracketmap[commodity['demandBracket']], + int(commodity['stock']) if commodity['stockBracket'] else '', + stockbracketmap[commodity['stockBracket']], + timestamp).encode('utf-8')) - # Format described here: https://bitbucket.org/kfsone/tradedangerous/wiki/Price%20Data - h = open(filename, 'wb') # codecs can't automatically handle line endings, so encode manually where required - h.write('#! trade.py import -\n# Created by {appname} {appversion} on {platform} for Cmdr {cmdr}.\n' - '#\n# \n\n' - '@ {system}/{starport}\n'.format( - appname=applongname, - appversion=appversion(), - platform=platform == 'darwin' and "Mac OS" or system(), - cmdr=data['commander']['name'].strip(), - system=data['lastSystem']['name'].strip(), - starport=data['lastStarport']['name'].strip() - ).encode('utf-8')) - - # sort commodities by category - bycategory = defaultdict(list) - for commodity in data['lastStarport']['commodities']: - bycategory[commodity['categoryname']].append(commodity) - - for category in sorted(bycategory): - h.write(' + {}\n'.format(category).encode('utf-8')) - # corrections to commodity names can change the sort order - for commodity in sorted(bycategory[category], key=itemgetter('name')): - h.write(' {:<23} {:7d} {:7d} {:9}{:1} {:8}{:1} {}\n'.format( - commodity['name'], - int(commodity['sellPrice']), - int(commodity['buyPrice']), - int(commodity['demand']) if commodity['demandBracket'] else '', - demandbracketmap[commodity['demandBracket']], - int(commodity['stock']) if commodity['stockBracket'] else '', - stockbracketmap[commodity['stockBracket']], - timestamp).encode('utf-8')) - - h.close() From c9555cce66f2eacc449673738cbf12a29412202e Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 16:27:22 +0000 Subject: [PATCH 14/41] td.py: Further re-factoring to pass flake8 checks The output of this was confirmed the 'same' as from before this work: 1. Generate the output. 2. `cut -c 1-68` to a copy. 3. diff those files. This is necessary so as to not have 'every' line be different due to the timestamp on it. --- td.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/td.py b/td.py index f75ddf93..cd2c0d3e 100644 --- a/td.py +++ b/td.py @@ -5,7 +5,6 @@ import time from collections import defaultdict from operator import itemgetter from platform import system -from sys import platform from companion import CAPIData from config import applongname, appversion, config @@ -20,6 +19,7 @@ stockbracketmap = {0: '-', 2: 'M', 3: 'H', } + def export(data: CAPIData) -> None: """Export market data in TD format.""" data_path = pathlib.Path(config.get_str('outdir')) @@ -31,31 +31,35 @@ def export(data: CAPIData) -> None: with open(data_path / data_filename, 'wb') as h: # Format described here: https://bitbucket.org/kfsone/tradedangerous/wiki/Price%20Data h.write('#! trade.py import -\n'.encode('utf-8')) - this_platform = 'darwin' and "Mac OS" or system_name() + this_platform = 'darwin' and "Mac OS" or system() cmdr_name = data['commander']['name'].strip() - h.write(f'# Created by {applongname} {appversion()} on {this_platform} for Cmdr {cmdr_name}.\n'.encode('utf-8')) - h.write('#\n# \n\n'.encode('utf-8')) + h.write( + f'# Created by {applongname} {appversion()} on {this_platform} for Cmdr {cmdr_name}.\n'.encode('utf-8') + ) + h.write( + '#\n# \n\n'.encode('utf-8') + ) system_name = data['lastSystem']['name'].strip() starport_name = data['lastStarport']['name'].strip() h.write(f'@ {system_name}/{starport_name}\n'.encode('utf-8')) # sort commodities by category - bycategory = defaultdict(list) + by_category = defaultdict(list) for commodity in data['lastStarport']['commodities']: - bycategory[commodity['categoryname']].append(commodity) + by_category[commodity['categoryname']].append(commodity) timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.strptime(data['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) - for category in sorted(bycategory): - h.write(' + {}\n'.format(category).encode('utf-8')) + for category in sorted(by_category): + h.write(f' + {format(category)}\n'.encode('utf-8')) # corrections to commodity names can change the sort order - for commodity in sorted(bycategory[category], key=itemgetter('name')): - h.write(' {:<23} {:7d} {:7d} {:9}{:1} {:8}{:1} {}\n'.format( - commodity['name'], - int(commodity['sellPrice']), - int(commodity['buyPrice']), - int(commodity['demand']) if commodity['demandBracket'] else '', - demandbracketmap[commodity['demandBracket']], - int(commodity['stock']) if commodity['stockBracket'] else '', - stockbracketmap[commodity['stockBracket']], - timestamp).encode('utf-8')) - + for commodity in sorted(by_category[category], key=itemgetter('name')): + h.write( + f" {commodity['name']:<23}" + f" {int(commodity['sellPrice']):7d}" + f" {int(commodity['buyPrice']):7d}" + f" {int(commodity['demand']) if commodity['demandBracket'] else '':9}" + f"{demandbracketmap[commodity['demandBracket']]:1}" + f" {int(commodity['stock']) if commodity['stockBracket'] else '':8}" + f"{stockbracketmap[commodity['stockBracket']]:1}" + f" {timestamp}\n".encode('utf-8') + ) From cc8e0bfd27225813f073447e6dc627aa215c9005 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 16:59:39 +0000 Subject: [PATCH 15/41] theme.py: flake8 pass Conversion from %-format to f-string means an `assert ..., string`` is now a condition and a `raise AssertionError(string)`. The problem being that f-string gets evaluated before the assert, but in this case the things the f-string relies on are only there if the assert triggers. --- theme.py | 72 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/theme.py b/theme.py index 4f60f976..de694d6b 100644 --- a/theme.py +++ b/theme.py @@ -1,20 +1,20 @@ -# -# Theme support -# -# Because of various ttk limitations this app is an unholy mix of Tk and ttk widgets. -# So can't use ttk's theme support. So have to change colors manually. -# +""" +Theme support. + +Because of various ttk limitations this app is an unholy mix of Tk and ttk widgets. +So can't use ttk's theme support. So have to change colors manually. +""" import os import sys import tkinter as tk from os.path import join -from tkinter import font as tkFont +from tkinter import font as tk_font from tkinter import ttk from config import config -from ttkHyperlinkLabel import HyperlinkLabel from EDMCLogging import get_main_logger +from ttkHyperlinkLabel import HyperlinkLabel logger = get_main_logger() @@ -65,6 +65,8 @@ elif sys.platform == 'linux': MWM_DECOR_MAXIMIZE = 1 << 6 class MotifWmHints(Structure): + """MotifWmHints structure.""" + _fields_ = [ ('flags', c_ulong), ('functions', c_ulong), @@ -99,16 +101,19 @@ elif sys.platform == 'linux': raise Exception("Can't find your display, can't continue") motif_wm_hints_property = XInternAtom(dpy, b'_MOTIF_WM_HINTS', False) - motif_wm_hints_normal = MotifWmHints(MWM_HINTS_FUNCTIONS | MWM_HINTS_DECORATIONS, - MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE, - MWM_DECOR_BORDER | MWM_DECOR_RESIZEH | MWM_DECOR_TITLE | MWM_DECOR_MENU | MWM_DECOR_MINIMIZE, - 0, 0) + motif_wm_hints_normal = MotifWmHints( + MWM_HINTS_FUNCTIONS | MWM_HINTS_DECORATIONS, + MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE, + MWM_DECOR_BORDER | MWM_DECOR_RESIZEH | MWM_DECOR_TITLE | MWM_DECOR_MENU | MWM_DECOR_MINIMIZE, + 0, 0 + ) motif_wm_hints_dark = MotifWmHints(MWM_HINTS_FUNCTIONS | MWM_HINTS_DECORATIONS, MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE, 0, 0, 0) - except: + except Exception: if __debug__: print_exc() + dpy = None @@ -124,8 +129,9 @@ class _Theme(object): self.default_ui_scale = None # None == not yet known self.startup_ui_scale = None - def register(self, widget): - # Note widget and children for later application of a theme. Note if the widget has explicit fg or bg attributes. + def register(self, widget): # noqa: CCR001, C901 + # Note widget and children for later application of a theme. Note if + # the widget has explicit fg or bg attributes. assert isinstance(widget, tk.Widget) or isinstance(widget, tk.BitmapImage), widget if not self.defaults: # Can't initialise this til window is created # Windows, MacOS @@ -160,7 +166,10 @@ class _Theme(object): if 'font' in widget.keys() and str(widget['font']) not in ['', self.defaults['entryfont']]: attribs.add('font') elif isinstance(widget, tk.Frame) or isinstance(widget, ttk.Frame) or isinstance(widget, tk.Canvas): - if ('background' in widget.keys() or isinstance(widget, tk.Canvas)) and widget['background'] not in ['', self.defaults['frame']]: + if ( + ('background' in widget.keys() or isinstance(widget, tk.Canvas)) + and widget['background'] not in ['', self.defaults['frame']] + ): attribs.add('bg') elif isinstance(widget, HyperlinkLabel): pass # Hack - HyperlinkLabel changes based on state, so skip @@ -245,12 +254,13 @@ class _Theme(object): 'foreground': config.get_str('dark_text'), 'activebackground': config.get_str('dark_text'), 'activeforeground': 'grey4', - 'disabledforeground': '#%02x%02x%02x' % (int(r/384), int(g/384), int(b/384)), + 'disabledforeground': f'#{int(r/384):02x}{int(g/384):02x}{int(b/384):02x}', 'highlight': config.get_str('dark_highlight'), - # Font only supports Latin 1 / Supplement / Extended, and a few General Punctuation and Mathematical Operators + # Font only supports Latin 1 / Supplement / Extended, and a + # few General Punctuation and Mathematical Operators # LANG: Label for commander name in main window 'font': (theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000 and - tkFont.Font(family='Euro Caps', size=10, weight=tkFont.NORMAL) or + tk_font.Font(family='Euro Caps', size=10, weight=tk_font.NORMAL) or 'TkDefaultFont'), } else: @@ -282,9 +292,11 @@ class _Theme(object): self._update_widget(child) # Apply current theme to a single widget - def _update_widget(self, widget): - assert widget in self.widgets, '%s %s "%s"' % ( - widget.winfo_class(), widget, 'text' in widget.keys() and widget['text']) + def _update_widget(self, widget): # noqa: CCR001, C901 + if widget not in self.widgets: + assert_str = f'{widget.winfo_class()} {widget} "{"text" in widget.keys() and widget["text"]}"' + raise AssertionError(assert_str) + attribs = self.widgets.get(widget, []) try: @@ -348,7 +360,7 @@ class _Theme(object): # Apply configured theme - def apply(self, root): + def apply(self, root): # noqa: CCR001 theme = config.get_int('theme') self._colors(root, theme) @@ -390,14 +402,14 @@ class _Theme(object): window.setAppearance_(appearance) elif sys.platform == 'win32': - GWL_STYLE = -16 - WS_MAXIMIZEBOX = 0x00010000 + GWL_STYLE = -16 # noqa: N806 # ctypes + WS_MAXIMIZEBOX = 0x00010000 # noqa: N806 # ctypes # tk8.5.9/win/tkWinWm.c:342 - GWL_EXSTYLE = -20 - WS_EX_APPWINDOW = 0x00040000 - WS_EX_LAYERED = 0x00080000 - GetWindowLongW = ctypes.windll.user32.GetWindowLongW - SetWindowLongW = ctypes.windll.user32.SetWindowLongW + GWL_EXSTYLE = -20 # noqa: N806 # ctypes + WS_EX_APPWINDOW = 0x00040000 # noqa: N806 # ctypes + WS_EX_LAYERED = 0x00080000 # noqa: N806 # ctypes + GetWindowLongW = ctypes.windll.user32.GetWindowLongW # noqa: N806 # ctypes + SetWindowLongW = ctypes.windll.user32.SetWindowLongW # noqa: N806 # ctypes root.overrideredirect(theme and 1 or 0) root.attributes("-transparentcolor", theme > 1 and 'grey4' or '') From 8728234625d9bb6b655382f412e72295ab6a6b09 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 17:36:10 +0000 Subject: [PATCH 16/41] theme.py: mypy pass mypy/typeshed still doesn't like 'generic' tk arguments on things like .configure(), so lots of `# type: ignore` used for those. --- theme.py | 73 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/theme.py b/theme.py index de694d6b..f49395d9 100644 --- a/theme.py +++ b/theme.py @@ -11,6 +11,7 @@ import tkinter as tk from os.path import join from tkinter import font as tk_font from tkinter import ttk +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Set, Tuple from config import config from EDMCLogging import get_main_logger @@ -18,6 +19,9 @@ from ttkHyperlinkLabel import HyperlinkLabel logger = get_main_logger() +if TYPE_CHECKING: + def _(x: str) -> str: ... + if __debug__: from traceback import print_exc @@ -29,7 +33,7 @@ if sys.platform == 'win32': import ctypes from ctypes.wintypes import DWORD, LPCVOID, LPCWSTR AddFontResourceEx = ctypes.windll.gdi32.AddFontResourceExW - AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID] + AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID] # type: ignore FR_PRIVATE = 0x10 FR_NOT_ENUM = 0x20 AddFontResourceEx(join(config.respath, u'EUROCAPS.TTF'), FR_PRIVATE, 0) @@ -119,17 +123,17 @@ elif sys.platform == 'linux': class _Theme(object): - def __init__(self): + def __init__(self) -> None: self.active = None # Starts out with no theme - self.minwidth = None - self.widgets = {} - self.widgets_pair = [] - self.defaults = {} - self.current = {} + self.minwidth: Optional[int] = None + self.widgets: Dict[tk.Widget, Set] = {} + self.widgets_pair: List = [] + self.defaults: Dict = {} + self.current: Dict = {} self.default_ui_scale = None # None == not yet known self.startup_ui_scale = None - def register(self, widget): # noqa: CCR001, C901 + def register(self, widget: tk.Widget) -> None: # noqa: CCR001, C901 # Note widget and children for later application of a theme. Note if # the widget has explicit fg or bg attributes. assert isinstance(widget, tk.Widget) or isinstance(widget, tk.BitmapImage), widget @@ -193,15 +197,17 @@ class _Theme(object): for child in widget.winfo_children(): self.register(child) - def register_alternate(self, pair, gridopts): + def register_alternate(self, pair: Tuple, gridopts: Dict) -> None: self.widgets_pair.append((pair, gridopts)) - def button_bind(self, widget, command, image=None): + def button_bind( + self, widget: tk.Widget, command: Callable, image: Optional[tk.BitmapImage] = None + ) -> None: widget.bind('', command) widget.bind('', lambda e: self._enter(e, image)) widget.bind('', lambda e: self._leave(e, image)) - def _enter(self, event, image): + def _enter(self, event: tk.Event, image: Optional[tk.BitmapImage]) -> None: widget = event.widget if widget and widget['state'] != tk.DISABLED: try: @@ -218,7 +224,7 @@ class _Theme(object): except Exception: logger.exception(f'Failure configuring image: {image=}') - def _leave(self, event, image): + def _leave(self, event: tk.Event, image: Optional[tk.BitmapImage]) -> None: widget = event.widget if widget and widget['state'] != tk.DISABLED: try: @@ -235,7 +241,7 @@ class _Theme(object): logger.exception(f'Failure configuring image: {image=}') # Set up colors - def _colors(self, root, theme): + def _colors(self, root: tk.Tk, theme: int) -> None: style = ttk.Style() if sys.platform == 'linux': style.theme_use('clam') @@ -281,10 +287,11 @@ class _Theme(object): # Apply current theme to a widget and its children, and register it for future updates - def update(self, widget): + def update(self, widget: tk.Widget) -> None: assert isinstance(widget, tk.Widget) or isinstance(widget, tk.BitmapImage), widget if not self.current: return # No need to call this for widgets created in plugin_app() + self.register(widget) self._update_widget(widget) if isinstance(widget, tk.Frame) or isinstance(widget, ttk.Frame): @@ -292,67 +299,67 @@ class _Theme(object): self._update_widget(child) # Apply current theme to a single widget - def _update_widget(self, widget): # noqa: CCR001, C901 + def _update_widget(self, widget: tk.Widget) -> None: # noqa: CCR001, C901 if widget not in self.widgets: assert_str = f'{widget.winfo_class()} {widget} "{"text" in widget.keys() and widget["text"]}"' raise AssertionError(assert_str) - attribs = self.widgets.get(widget, []) + attribs: Set = self.widgets.get(widget, set()) try: if isinstance(widget, tk.BitmapImage): # not a widget if 'fg' not in attribs: - widget.configure(foreground=self.current['foreground']), + widget.configure(foreground=self.current['foreground']), # type: ignore if 'bg' not in attribs: - widget.configure(background=self.current['background']) + widget.configure(background=self.current['background']) # type: ignore elif 'cursor' in widget.keys() and str(widget['cursor']) not in ['', 'arrow']: # Hack - highlight widgets like HyperlinkLabel with a non-default cursor if 'fg' not in attribs: - widget.configure(foreground=self.current['highlight']), + widget.configure(foreground=self.current['highlight']), # type: ignore if 'insertbackground' in widget.keys(): # tk.Entry - widget.configure(insertbackground=self.current['foreground']), + widget.configure(insertbackground=self.current['foreground']), # type: ignore if 'bg' not in attribs: - widget.configure(background=self.current['background']) + widget.configure(background=self.current['background']) # type: ignore if 'highlightbackground' in widget.keys(): # tk.Entry - widget.configure(highlightbackground=self.current['background']) + widget.configure(highlightbackground=self.current['background']) # type: ignore if 'font' not in attribs: - widget.configure(font=self.current['font']) + widget.configure(font=self.current['font']) # type: ignore elif 'activeforeground' in widget.keys(): # e.g. tk.Button, tk.Label, tk.Menu if 'fg' not in attribs: - widget.configure(foreground=self.current['foreground'], + widget.configure(foreground=self.current['foreground'], # type: ignore activeforeground=self.current['activeforeground'], disabledforeground=self.current['disabledforeground']) if 'bg' not in attribs: - widget.configure(background=self.current['background'], + widget.configure(background=self.current['background'], # type: ignore activebackground=self.current['activebackground']) if sys.platform == 'darwin' and isinstance(widget, tk.Button): - widget.configure(highlightbackground=self.current['background']) + widget.configure(highlightbackground=self.current['background']) # type: ignore if 'font' not in attribs: - widget.configure(font=self.current['font']) + widget.configure(font=self.current['font']) # type: ignore elif 'foreground' in widget.keys(): # e.g. ttk.Label if 'fg' not in attribs: - widget.configure(foreground=self.current['foreground']), + widget.configure(foreground=self.current['foreground']), # type: ignore if 'bg' not in attribs: - widget.configure(background=self.current['background']) + widget.configure(background=self.current['background']) # type: ignore if 'font' not in attribs: - widget.configure(font=self.current['font']) + widget.configure(font=self.current['font']) # type: ignore elif 'background' in widget.keys() or isinstance(widget, tk.Canvas): # e.g. Frame, Canvas if 'bg' not in attribs: - widget.configure(background=self.current['background'], + widget.configure(background=self.current['background'], # type: ignore highlightbackground=self.current['disabledforeground']) except Exception: @@ -360,7 +367,7 @@ class _Theme(object): # Apply configured theme - def apply(self, root): # noqa: CCR001 + def apply(self, root: tk.Tk) -> None: # noqa: CCR001 theme = config.get_int('theme') self._colors(root, theme) @@ -411,7 +418,7 @@ class _Theme(object): GetWindowLongW = ctypes.windll.user32.GetWindowLongW # noqa: N806 # ctypes SetWindowLongW = ctypes.windll.user32.SetWindowLongW # noqa: N806 # ctypes - root.overrideredirect(theme and 1 or 0) + root.overrideredirect(theme and True or False) root.attributes("-transparentcolor", theme > 1 and 'grey4' or '') root.withdraw() root.update_idletasks() # Size and windows styles get recalculated here From 0dbbb3882134981f2ca3e2e7757518479b5b1280 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 17:50:47 +0000 Subject: [PATCH 17/41] theme.py: Just use 'dict' style configuration of widgets It turns out that you don't need to call `.configure(keyword=...)` on tk widgets. You can just treat them as a dict with the available option as a key. This neatly gets rid of the type hint issues. --- theme.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/theme.py b/theme.py index f49395d9..de7ef696 100644 --- a/theme.py +++ b/theme.py @@ -310,57 +310,58 @@ class _Theme(object): if isinstance(widget, tk.BitmapImage): # not a widget if 'fg' not in attribs: - widget.configure(foreground=self.current['foreground']), # type: ignore + widget['foreground'] = self.current['foreground'] if 'bg' not in attribs: - widget.configure(background=self.current['background']) # type: ignore + widget['background'] = self.current['background'] elif 'cursor' in widget.keys() and str(widget['cursor']) not in ['', 'arrow']: # Hack - highlight widgets like HyperlinkLabel with a non-default cursor if 'fg' not in attribs: - widget.configure(foreground=self.current['highlight']), # type: ignore + widget['foreground'] = self.current['highlight'] if 'insertbackground' in widget.keys(): # tk.Entry - widget.configure(insertbackground=self.current['foreground']), # type: ignore + widget['insertbackground'] = self.current['foreground'] if 'bg' not in attribs: - widget.configure(background=self.current['background']) # type: ignore + widget['background'] = self.current['background'] if 'highlightbackground' in widget.keys(): # tk.Entry - widget.configure(highlightbackground=self.current['background']) # type: ignore + widget['highlightbackground'] = self.current['background'] if 'font' not in attribs: - widget.configure(font=self.current['font']) # type: ignore + widget['font'] = self.current['font'] elif 'activeforeground' in widget.keys(): # e.g. tk.Button, tk.Label, tk.Menu if 'fg' not in attribs: - widget.configure(foreground=self.current['foreground'], # type: ignore - activeforeground=self.current['activeforeground'], - disabledforeground=self.current['disabledforeground']) + widget['foreground'] = self.current['foreground'] + widget['activeforeground'] = self.current['activeforeground'], + widget['disabledforeground'] = self.current['disabledforeground'] if 'bg' not in attribs: - widget.configure(background=self.current['background'], # type: ignore - activebackground=self.current['activebackground']) + widget['background'] = self.current['background'] + widget['activebackground'] = self.current['activebackground'] if sys.platform == 'darwin' and isinstance(widget, tk.Button): - widget.configure(highlightbackground=self.current['background']) # type: ignore + widget['highlightbackground'] = self.current['background'] if 'font' not in attribs: - widget.configure(font=self.current['font']) # type: ignore + widget['font'] = self.current['font'] + elif 'foreground' in widget.keys(): # e.g. ttk.Label if 'fg' not in attribs: - widget.configure(foreground=self.current['foreground']), # type: ignore + widget['foreground'] = self.current['foreground'] if 'bg' not in attribs: - widget.configure(background=self.current['background']) # type: ignore + widget['background'] = self.current['background'] if 'font' not in attribs: - widget.configure(font=self.current['font']) # type: ignore + widget['font'] = self.current['font'] elif 'background' in widget.keys() or isinstance(widget, tk.Canvas): # e.g. Frame, Canvas if 'bg' not in attribs: - widget.configure(background=self.current['background'], # type: ignore - highlightbackground=self.current['disabledforeground']) + widget['background'] = self.current['background'] + widget['highlightbackground'] = self.current['disabledforeground'] except Exception: logger.exception(f'Plugin widget issue ? {widget=}') From e280d6c2833c25867b8139490e68ddf056477917 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 18:17:51 +0000 Subject: [PATCH 18/41] ttkHyperlinklabel.py: Initial flake8 pass (and some mypy) This fixes an apparently harmless bug in the `openurl()` function defined in this module (it's not part of the class). 1. On `win32` lookup the user setting for opening HTTPS URLs. 2. If that doesn't look like IE or Edge... 3. Set `cls` to the value for that. 4. Now look up the 'use this command' for *that* ... 5. And if it doesn't have `iexplore` in it... 6. Use `subprocess.Popen()` to invoke that browser with the given URL. The problem is that step 6 still tries to use `buf.value`. But `buf` is no longer present as it was from before 5989acd0d3263e54429ff99769ff73a20476d863 changed over to `winreg`. It should be just `value` from the winreg calls. That exception is then caught and ignored, and it ends up just running `webbrowser.open(url)` anyway. To be honest, this feels like we should just make this an unconditional call to `webbrowser.open(url)` now, given apparently no-one's complained about it always actually using that not working for them. Given Edge is Chrome-based now, and any supported OS should have Edge, Chrome or Firefox (OK, maybe Safari and some others) as the HTTPS browser, I don't see this being an issue. --- ttkHyperlinkLabel.py | 66 +++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index 8dd08644..5a13a5fc 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -1,7 +1,23 @@ +""" +A clickable ttk label for HTTP links. + +In addition to standard ttk.Label arguments, takes the following arguments: + url: The URL as a string that the user will be sent to on clicking on + non-empty label text. If url is a function it will be called on click with + the current label text and should return the URL as a string. + underline: If True/False the text is always/never underlined. If None (the + default) the text is underlined only on hover. + popup_copy: Whether right-click on non-empty label text pops up a context + menu with a 'Copy' option. Defaults to no context menu. If popup_copy is a + function it will be called with the current label text and should return a + boolean. + +May be imported by plugins +""" import sys import tkinter as tk import webbrowser -from tkinter import font as tkFont +from tkinter import font as tk_font from tkinter import ttk if sys.platform == 'win32': @@ -10,15 +26,10 @@ if sys.platform == 'win32': # A clickable ttk Label # -# In addition to standard ttk.Label arguments, takes the following arguments: -# url: The URL as a string that the user will be sent to on clicking on non-empty label text. If url is a function it will be called on click with the current label text and should return the URL as a string. -# underline: If True/False the text is always/never underlined. If None (the default) the text is underlined only on hover. -# popup_copy: Whether right-click on non-empty label text pops up a context menu with a 'Copy' option. Defaults to no context menu. If popup_copy is a function it will be called with the current label text and should return a boolean. -# -# May be imported by plugins class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object): + """Clickable label for HTTP links.""" def __init__(self, master=None, **kw): self.url = 'url' in kw and kw.pop('url') or None @@ -51,8 +62,8 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) text=kw.get('text'), font=kw.get('font', ttk.Style().lookup('TLabel', 'font'))) - # Change cursor and appearance depending on state and text - def configure(self, cnf=None, **kw): + def configure(self, cnf=None, **kw): # noqa: CCR001 + """Change cursor and appearance depending on state and text.""" # This class' state for thing in ['url', 'popup_copy', 'underline']: if thing in kw: @@ -71,7 +82,7 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) if 'font' in kw: self.font_n = kw['font'] - self.font_u = tkFont.Font(font=self.font_n) + self.font_u = tk_font.Font(font=self.font_n) self.font_u.configure(underline=True) kw['font'] = self.underline is True and self.font_u or self.font_n @@ -86,7 +97,13 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) super(HyperlinkLabel, self).configure(cnf, **kw) - def __setitem__(self, key, value): + def __setitem__(self, key, value) -> None: + """ + Allow for dict member style setting of options. + + :param key: option name + :param value: option value + """ self.configure(None, **{key: value}) def _enter(self, event): @@ -108,17 +125,23 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) if self['text'] and (self.popup_copy(self['text']) if callable(self.popup_copy) else self.popup_copy): self.menu.post(sys.platform == 'darwin' and event.x_root + 1 or event.x_root, event.y_root) - def copy(self): + def copy(self) -> None: + """Copy the current text to the clipboard.""" self.clipboard_clear() self.clipboard_append(self['text']) -def openurl(url): +def openurl(url) -> None: # noqa: CCR001 + """ + Open the given URL in appropriate browser. + + :param url: URL to open. + """ if sys.platform == 'win32': + # FIXME: Is still still true with supported Windows 10 and 11 ? # On Windows webbrowser.open calls os.startfile which calls ShellExecute which can't handle long arguments, # so discover and launch the browser directly. # https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553 - try: hkey = OpenKeyEx(HKEY_CURRENT_USER, r'Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice') @@ -128,23 +151,28 @@ def openurl(url): # IE and Edge can't handle long arguments so just use webbrowser.open and hope # https://blogs.msdn.microsoft.com/ieinternals/2014/08/13/url-length-limits/ cls = None + else: cls = value - except: + + except Exception: cls = 'https' if cls: try: - hkey = OpenKeyEx(HKEY_CLASSES_ROOT, r'%s\shell\open\command' % cls) + hkey = OpenKeyEx(HKEY_CLASSES_ROOT, rf'{cls}\shell\open\command') (value, typ) = QueryValueEx(hkey, None) CloseKey(hkey) if 'iexplore' not in value.lower(): if '%1' in value: - subprocess.Popen(buf.value.replace('%1', url)) + subprocess.Popen(value.replace('%1', url)) + else: - subprocess.Popen('%s "%s"' % (buf.value, url)) + subprocess.Popen(f'{value} "{url}"') + return - except: + + except Exception: pass webbrowser.open(url) From b57a8f99ae151c630b3d4594320cc2241f0e13f2 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 3 Dec 2022 21:04:50 +0000 Subject: [PATCH 19/41] ttkHyperlinkLabel: Now passing flake8 & mypy * `openurl()` - Don't pass `None` as second parameter to `QueryValueEx()`. Passing `''` was tested as still working. --- ttkHyperlinkLabel.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index 5a13a5fc..e29caebf 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -19,19 +19,22 @@ import tkinter as tk import webbrowser from tkinter import font as tk_font from tkinter import ttk +from typing import TYPE_CHECKING, Any, Optional if sys.platform == 'win32': import subprocess from winreg import HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, CloseKey, OpenKeyEx, QueryValueEx -# A clickable ttk Label -# +if TYPE_CHECKING: + def _(x: str) -> str: ... -class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object): +# FIXME: Split this into multi-file module to separate the platforms +class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object): # type: ignore """Clickable label for HTTP links.""" - def __init__(self, master=None, **kw): + # NB: do **NOT** try `**kw: Dict`, it causes more trouble than it's worth. + def __init__(self, master: Optional[tk.Tk] = None, **kw) -> 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 @@ -44,8 +47,9 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) kw['background'] = kw.pop('background', 'systemDialogBackgroundActive') kw['anchor'] = kw.pop('anchor', tk.W) # like ttk.Label tk.Label.__init__(self, master, **kw) + else: - ttk.Label.__init__(self, master, **kw) + ttk.Label.__init__(self, master, **kw) # type: ignore self.bind('', self._click) @@ -62,7 +66,10 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) text=kw.get('text'), font=kw.get('font', ttk.Style().lookup('TLabel', 'font'))) - def configure(self, cnf=None, **kw): # noqa: CCR001 + # NB: do **NOT** try `**kw: Dict`, it causes more trouble than it's worth. + def configure( # noqa: CCR001 + self, cnf: dict[str, Any] | None = None, **kw + ) -> dict[str, tuple[str, str, str, Any, Any]] | None: """Change cursor and appearance depending on state and text.""" # This class' state for thing in ['url', 'popup_copy', 'underline']: @@ -95,9 +102,9 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) kw['cursor'] = (sys.platform == 'darwin' and 'notallowed') or ( sys.platform == 'win32' and 'no') or 'circle' - super(HyperlinkLabel, self).configure(cnf, **kw) + return super(HyperlinkLabel, self).configure(cnf, **kw) - def __setitem__(self, key, value) -> None: + def __setitem__(self, key: str, value) -> None: """ Allow for dict member style setting of options. @@ -106,22 +113,22 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) """ self.configure(None, **{key: value}) - def _enter(self, event): + def _enter(self, event: tk.Event) -> None: if self.url and self.underline is not False and str(self['state']) != tk.DISABLED: super(HyperlinkLabel, self).configure(font=self.font_u) - def _leave(self, event): + def _leave(self, event: tk.Event) -> None: if not self.underline: super(HyperlinkLabel, self).configure(font=self.font_n) - def _click(self, event): + def _click(self, event: tk.Event) -> None: if self.url and self['text'] and str(self['state']) != tk.DISABLED: url = self.url(self['text']) if callable(self.url) else self.url if url: self._leave(event) # Remove underline before we change window to browser openurl(url) - def _contextmenu(self, event): + def _contextmenu(self, event: tk.Event) -> None: if self['text'] and (self.popup_copy(self['text']) if callable(self.popup_copy) else self.popup_copy): self.menu.post(sys.platform == 'darwin' and event.x_root + 1 or event.x_root, event.y_root) @@ -131,7 +138,7 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) self.clipboard_append(self['text']) -def openurl(url) -> None: # noqa: CCR001 +def openurl(url: str) -> None: # noqa: CCR001 """ Open the given URL in appropriate browser. @@ -161,7 +168,7 @@ def openurl(url) -> None: # noqa: CCR001 if cls: try: hkey = OpenKeyEx(HKEY_CLASSES_ROOT, rf'{cls}\shell\open\command') - (value, typ) = QueryValueEx(hkey, None) + (value, typ) = QueryValueEx(hkey, '') CloseKey(hkey) if 'iexplore' not in value.lower(): if '%1' in value: From b38044928a82685bc5075abb985b3b4fbf230e4f Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 14:23:53 +0000 Subject: [PATCH 20/41] update.py: Now passes flake8 **BUG FOUND** The code in `Updater.check_appcast()` assumes that `semantic_version.SimpleSpec.select()` will say "5.6.1 is an upgrade for this 5.6.0". But it doesn't. It's looking for *matches*. So this needs to be a proper loop/compare *and* should only take into account options *for the current platform*. --- update.py | 101 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/update.py b/update.py index 00fe9e6d..5d9c9b42 100644 --- a/update.py +++ b/update.py @@ -1,15 +1,22 @@ +"""Checking for updates to this application.""" import os -from os.path import dirname, join import sys import threading +from os.path import dirname, join from traceback import print_exc -import semantic_version from typing import TYPE_CHECKING, Optional +from xml.etree import ElementTree + +import requests +import semantic_version + if TYPE_CHECKING: import tkinter as tk -# ensure registry is set up on Windows before we start -from config import appname, appversion_nobuild, config, update_feed +from config import appversion_nobuild, config, update_feed +from EDMCLogging import get_main_logger + +logger = get_main_logger() class EDMCVersion(object): @@ -25,6 +32,7 @@ class EDMCVersion(object): sv: semantic_version.base.Version semantic_version object for this version """ + def __init__(self, version: str, title: str, sv: semantic_version.base.Version): self.version: str = version self.title: str = title @@ -33,30 +41,32 @@ class EDMCVersion(object): class Updater(object): """ - Updater class to handle checking for updates, whether using internal code - or an external library such as WinSparkle on win32. + Handle checking for updates. + + This is used whether using internal code or an external library such as + WinSparkle on win32. """ def shutdown_request(self) -> None: - """ - Receive (Win)Sparkle shutdown request and send it to parent. - :rtype: None - """ + """Receive (Win)Sparkle shutdown request and send it to parent.""" if not config.shutting_down: self.root.event_generate('<>', when="tail") def use_internal(self) -> bool: """ - :return: if internal update checks should be used. - :rtype: bool + Signal if internal update checks should be used. + + :return: bool """ if self.provider == 'internal': return True return False - def __init__(self, tkroot: 'tk.Tk'=None, provider: str='internal'): + def __init__(self, tkroot: 'tk.Tk' = None, provider: str = 'internal'): """ + Initialise an Updater instance. + :param tkroot: reference to the root window of the GUI :param provider: 'internal' or other string if not """ @@ -65,7 +75,7 @@ class Updater(object): self.thread: threading.Thread = None if self.use_internal(): - return + return if sys.platform == 'win32': import ctypes @@ -93,7 +103,7 @@ class Updater(object): # Get WinSparkle running self.updater.win_sparkle_init() - except Exception as ex: + except Exception: print_exc() self.updater = None @@ -101,19 +111,24 @@ class Updater(object): if sys.platform == 'darwin': import objc + try: - objc.loadBundle('Sparkle', globals(), join(dirname(sys.executable), os.pardir, 'Frameworks', 'Sparkle.framework')) - self.updater = SUUpdater.sharedUpdater() - except: + objc.loadBundle( + 'Sparkle', globals(), join(dirname(sys.executable), os.pardir, 'Frameworks', 'Sparkle.framework') + ) + # loadBundle presumably supplies `SUUpdater` + self.updater = SUUpdater.sharedUpdater() # noqa: F821 + + except Exception: # can't load framework - not frozen or not included in app bundle? print_exc() self.updater = None - def setAutomaticUpdatesCheck(self, onoroff: bool) -> None: + def set_automatic_updates_check(self, onoroff: bool) -> None: """ - Helper to set (Win)Sparkle to perform automatic update checks, or not. + Set (Win)Sparkle to perform automatic update checks, or not. + :param onoroff: bool for if we should have the library check or not. - :return: None """ if self.use_internal(): return @@ -124,13 +139,10 @@ class Updater(object): if sys.platform == 'darwin' and self.updater: self.updater.SUEnableAutomaticChecks(onoroff) - def checkForUpdates(self) -> None: - """ - Trigger the requisite method to check for an update. - :return: None - """ + def check_for_updates(self) -> None: + """Trigger the requisite method to check for an update.""" if self.use_internal(): - self.thread = threading.Thread(target = self.worker, name = 'update worker') + self.thread = threading.Thread(target=self.worker, name='update worker') self.thread.daemon = True self.thread.start() @@ -142,27 +154,27 @@ class Updater(object): def check_appcast(self) -> Optional[EDMCVersion]: """ - Manually (no Sparkle or WinSparkle) check the update_feed appcast file - to see if any listed version is semantically greater than the current + Manually (no Sparkle or WinSparkle) check the update_feed appcast file. + + Checks if any listed version is semantically greater than the current running version. :return: EDMCVersion or None if no newer version found """ - import requests - from xml.etree import ElementTree - newversion = None items = {} try: r = requests.get(update_feed, timeout=10) + except requests.RequestException as ex: - print('Error retrieving update_feed file: {}'.format(str(ex)), file=sys.stderr) + logger.exception(f'Error retrieving update_feed file: {ex}') return None try: feed = ElementTree.fromstring(r.text) + except SyntaxError as ex: - print('Syntax error in update_feed file: {}'.format(str(ex)), file=sys.stderr) + logger.exception(f'Syntax error in update_feed file: {ex}') return None @@ -171,9 +183,10 @@ class Updater(object): # This will change A.B.C.D to A.B.C+D sv = semantic_version.Version.coerce(ver) - items[sv] = EDMCVersion(version=ver, # sv might have mangled version - title=item.find('title').text, - sv=sv + items[sv] = EDMCVersion( + version=ver, # sv might have mangled version + title=item.find('title').text, + sv=sv ) # Look for any remaining version greater than appversion @@ -185,22 +198,16 @@ class Updater(object): return None def worker(self) -> None: - """ - Thread worker to perform internal update checking and update GUI - status if a newer version is found. - :return: None - """ + """Perform internal update checking & update GUI status if needs be.""" newversion = self.check_appcast() if newversion: - # TODO: Surely we can do better than this - # nametowidget('.{}.status'.format(appname.lower()))['text'] - self.root.nametowidget('.{}.status'.format(appname.lower()))['text'] = newversion.title + ' is available' + self.root.children['status']['text'] = newversion.title + ' is available' self.root.update_idletasks() def close(self) -> None: """ - Handles the EDMarketConnector.AppWindow.onexit() request. + Handle the EDMarketConnector.AppWindow.onexit() request. NB: We just 'pass' here because: 1) We might have a worker() going, but no way to make that @@ -208,7 +215,5 @@ class Updater(object): 2) If we're running frozen then we're using (Win)Sparkle to check and *it* might have asked this whole application to quit, in which case we don't want to ask *it* to quit - - :return: None """ pass From eefb56c2c040b0792d90d3942e085752e886e61a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 14:30:37 +0000 Subject: [PATCH 21/41] update.py: Check appcast version is correct platform --- update.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/update.py b/update.py index 5d9c9b42..8b24ed08 100644 --- a/update.py +++ b/update.py @@ -178,8 +178,22 @@ class Updater(object): return None + if sys.platform == 'darwin': + sparkle_platform = 'macos' + + else: + # For *these* purposes anything else is the same as 'windows', as + # non-win32 would be running from source. + sparkle_platform = 'windows' + for item in feed.findall('channel/item'): ver = item.find('enclosure').attrib.get('{http://www.andymatuschak.org/xml-namespaces/sparkle}version') + ver_platform = item.find('enclosure').attrib.get( + '{http://www.andymatuschak.org/xml-namespaces/sparkle}os' + ) + if ver_platform != sparkle_platform: + continue + # This will change A.B.C.D to A.B.C+D sv = semantic_version.Version.coerce(ver) From 3cc9aed724745e804048a6d824512776976d1888 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 14:35:49 +0000 Subject: [PATCH 22/41] EDMarketConnector: Updated for update.py function renames --- EDMarketConnector.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index ef97b3a2..51a404da 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -558,7 +558,7 @@ class AppWindow(object): # https://www.tcl.tk/man/tcl/TkCmd/menu.htm self.system_menu = tk.Menu(self.menubar, name='apple') self.system_menu.add_command(command=lambda: self.w.call('tk::mac::standardAboutPanel')) - self.system_menu.add_command(command=lambda: self.updater.checkForUpdates()) + self.system_menu.add_command(command=lambda: self.updater.check_for_updates()) self.menubar.add_cascade(menu=self.system_menu) self.file_menu = tk.Menu(self.menubar, name='file') self.file_menu.add_command(command=self.save_raw) @@ -601,7 +601,7 @@ 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.checkForUpdates()) + 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) @@ -724,7 +724,7 @@ class AppWindow(object): self.updater = update.Updater(tkroot=self.w, provider='external') else: self.updater = update.Updater(tkroot=self.w, provider='internal') - self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps + self.updater.check_for_updates() # Sparkle / WinSparkle does this automatically for packaged apps # Migration from <= 3.30 for username in config.get_list('fdev_usernames', default=[]): @@ -733,7 +733,6 @@ class AppWindow(object): config.delete('username', suppress=True) config.delete('password', suppress=True) config.delete('logdir', suppress=True) - self.postprefs(False) # Companion login happens in callback from monitor self.toggle_suit_row(visible=False) @@ -1382,7 +1381,7 @@ 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.setAutomaticUpdatesCheck(False) + self.updater.set_automatic_updates_check(False) logger.info('Monitor: Disable WinSparkle automatic update checks') # Can't start dashboard monitoring @@ -1433,7 +1432,7 @@ 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.setAutomaticUpdatesCheck(True) + self.updater.set_automatic_updates_check(True) logger.info('Monitor: Enable WinSparkle automatic update checks') def auth(self, event=None) -> None: From eab2f3536006913c90f5df862b719fc14808da20 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 14:43:00 +0000 Subject: [PATCH 23/41] update.py: Minor whitespace correction --- update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.py b/update.py index 8b24ed08..f8edad21 100644 --- a/update.py +++ b/update.py @@ -206,9 +206,9 @@ class Updater(object): # Look for any remaining version greater than appversion simple_spec = semantic_version.SimpleSpec(f'>{appversion_nobuild()}') newversion = simple_spec.select(items.keys()) - if newversion: return items[newversion] + return None def worker(self) -> None: From 2ac055e8f06cddb1f1f9ba55feeea25114bd7845 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 14:49:51 +0000 Subject: [PATCH 24/41] update.py: Yes, we have to use `.nametowidget()` 'status' is a child of 'edmarketconnector', not of `self.root`. And if we were to do `self.root.children['edmarketconnector'].children['status']` then we might as well use `.nametowidget()`. --- update.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/update.py b/update.py index f8edad21..4277598f 100644 --- a/update.py +++ b/update.py @@ -13,7 +13,7 @@ import semantic_version if TYPE_CHECKING: import tkinter as tk -from config import appversion_nobuild, config, update_feed +from config import appname, appversion_nobuild, config, update_feed from EDMCLogging import get_main_logger logger = get_main_logger() @@ -216,7 +216,8 @@ class Updater(object): newversion = self.check_appcast() if newversion: - self.root.children['status']['text'] = newversion.title + ' is available' + status = self.root.nametowidget(f'.{appname.lower()}.status') + status['text'] = newversion.title + ' is available' self.root.update_idletasks() def close(self) -> None: From 3358babe00b905d33238c58312e2dd99c32eee3c Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 14:55:22 +0000 Subject: [PATCH 25/41] EDMarketConnector: Move tk 'status' Label to its own section --- EDMarketConnector.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 51a404da..60e9f546 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -529,17 +529,19 @@ class AppWindow(object): # LANG: Update button in main window self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED) self.theme_button = tk.Label(frame, width=32 if sys.platform == 'darwin' else 28, state=tk.DISABLED) - self.status = tk.Label(frame, name='status', anchor=tk.W) ui_row = frame.grid_size()[1] self.button.grid(row=ui_row, columnspan=2, sticky=tk.NSEW) self.theme_button.grid(row=ui_row, columnspan=2, sticky=tk.NSEW) theme.register_alternate((self.button, self.theme_button, self.theme_button), {'row': ui_row, 'columnspan': 2, 'sticky': tk.NSEW}) - self.status.grid(columnspan=2, sticky=tk.EW) self.button.bind('', self.capi_request_data) theme.button_bind(self.theme_button, self.capi_request_data) + # Bottom 'status' line. + self.status = tk.Label(frame, name='status', anchor=tk.W) + self.status.grid(columnspan=2, sticky=tk.EW) + for child in frame.winfo_children(): child.grid_configure(padx=self.PADX, pady=( sys.platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0) From e419e6dca0fa4f042770efd4c3e5721e2591d5ae Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 14:56:58 +0000 Subject: [PATCH 26/41] update.py: Technically Updater.root can be None, so check this The only current users of the code that blindly uses `self.root` are for the GUI application, but let's add the checks anyway. --- update.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/update.py b/update.py index 4277598f..ce8e4924 100644 --- a/update.py +++ b/update.py @@ -49,7 +49,7 @@ class Updater(object): def shutdown_request(self) -> None: """Receive (Win)Sparkle shutdown request and send it to parent.""" - if not config.shutting_down: + if not config.shutting_down and self.root: self.root.event_generate('<>', when="tail") def use_internal(self) -> bool: @@ -63,14 +63,14 @@ class Updater(object): return False - def __init__(self, tkroot: 'tk.Tk' = None, provider: str = 'internal'): + def __init__(self, tkroot: Optional['tk.Tk'] = None, provider: str = 'internal'): """ Initialise an Updater instance. :param tkroot: reference to the root window of the GUI :param provider: 'internal' or other string if not """ - self.root: 'tk.Tk' = tkroot + self.root: Optional['tk.Tk'] = tkroot self.provider: str = provider self.thread: threading.Thread = None @@ -215,7 +215,7 @@ class Updater(object): """Perform internal update checking & update GUI status if needs be.""" newversion = self.check_appcast() - if newversion: + if newversion and self.root: status = self.root.nametowidget(f'.{appname.lower()}.status') status['text'] = newversion.title + ' is available' self.root.update_idletasks() From 74ebba20b4b2a3a2c5da36f07cffaf987c85a4b8 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 15:08:38 +0000 Subject: [PATCH 27/41] update.py: Remove unused 'global root' & some more typing `global root` was never removed after `self.root` became a thing. --- update.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/update.py b/update.py index ce8e4924..a44f4426 100644 --- a/update.py +++ b/update.py @@ -72,7 +72,7 @@ class Updater(object): """ self.root: Optional['tk.Tk'] = tkroot self.provider: str = provider - self.thread: threading.Thread = None + self.thread: Optional[threading.Thread] = None if self.use_internal(): return @@ -81,7 +81,7 @@ class Updater(object): import ctypes try: - self.updater = ctypes.cdll.WinSparkle + self.updater: Optional[ctypes.CDLL] = ctypes.cdll.WinSparkle # Set the appcast URL self.updater.win_sparkle_set_appcast_url(update_feed.encode()) @@ -94,8 +94,6 @@ class Updater(object): self.updater.win_sparkle_set_app_build_version(str(appversion_nobuild())) # set up shutdown callback - global root - root = tkroot self.callback_t = ctypes.CFUNCTYPE(None) # keep reference self.callback_fn = self.callback_t(self.shutdown_request) self.updater.win_sparkle_set_shutdown_request_callback(self.callback_fn) From 04145146d7f71df62d96954bf0d97b1ccfa09e13 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 15:12:46 +0000 Subject: [PATCH 28/41] update.py: Finaly mypy pass * It's possible the `xml` code could be changed to make types work. But the code works, ignore types on those calls. --- update.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/update.py b/update.py index a44f4426..bbd65ab2 100644 --- a/update.py +++ b/update.py @@ -185,8 +185,11 @@ class Updater(object): sparkle_platform = 'windows' for item in feed.findall('channel/item'): - ver = item.find('enclosure').attrib.get('{http://www.andymatuschak.org/xml-namespaces/sparkle}version') - ver_platform = item.find('enclosure').attrib.get( + # xml is a pain with types, hence these ignores + ver = item.find('enclosure').attrib.get( # type: ignore + '{http://www.andymatuschak.org/xml-namespaces/sparkle}version' + ) + ver_platform = item.find('enclosure').attrib.get( # type: ignore '{http://www.andymatuschak.org/xml-namespaces/sparkle}os' ) if ver_platform != sparkle_platform: @@ -196,8 +199,8 @@ class Updater(object): sv = semantic_version.Version.coerce(ver) items[sv] = EDMCVersion( - version=ver, # sv might have mangled version - title=item.find('title').text, + version=str(ver), # sv might have mangled version + title=item.find('title').text, # type: ignore sv=sv ) From c5a29439662e87d8fdb58ac2556a11af8807e649 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 15:14:31 +0000 Subject: [PATCH 29/41] util_ships.py: Add required blank line before function --- util_ships.py | 1 + 1 file changed, 1 insertion(+) diff --git a/util_ships.py b/util_ships.py index 204e21f5..bd3ae3df 100644 --- a/util_ships.py +++ b/util_ships.py @@ -1,6 +1,7 @@ """Utility functions relating to ships.""" from edmc_data import ship_name_map + def ship_file_name(ship_name: str, ship_type: str) -> str: """Return a ship name suitable for a filename.""" name = str(ship_name or ship_name_map.get(ship_type.lower(), ship_type)).strip() From 19114fdc3ad67f50945672ee38951d4b33da5c02 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 15:15:14 +0000 Subject: [PATCH 30/41] config/linux: Remove unused TYPE_CHECKING import --- config/linux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/linux.py b/config/linux.py index 58c0f7cd..04087b32 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 TYPE_CHECKING, List, Optional, Union +from typing import List, Optional, Union from config import AbstractConfig, appname, logger From 62ed12eba32fed228e770c57f0f4a3845cef86b5 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 15:36:00 +0000 Subject: [PATCH 31/41] plugins/eddb: flake8 pass --- plugins/eddb.py | 78 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/plugins/eddb.py b/plugins/eddb.py index 111d768c..b40d6855 100644 --- a/plugins/eddb.py +++ b/plugins/eddb.py @@ -1,8 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Station display and eddb.io lookup -# - +"""Station display and eddb.io lookup.""" # Tests: # # As there's a lot of state tracking in here, need to ensure (at least) @@ -38,15 +34,15 @@ # Thus you **MUST** check if any imports you add in this file are only # referenced in this file (or only in any other core plugin), and if so... # -# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py` -# SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER -# INSTALLATION ON WINDOWS. +# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN +# `Build-exe-and-msi.py` SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN +# AN END-USER INSTALLATION ON WINDOWS. # # # ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# # ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# import sys -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Optional import requests @@ -79,6 +75,13 @@ this.on_foot = False def system_url(system_name: str) -> str: + """ + Construct an appropriate EDDB.IO URL for the provided system. + + :param system_name: Will be overridden with `this.system_address` if that + is set. + :return: The URL, empty if no data was available to construct it. + """ if this.system_address: return requests.utils.requote_uri(f'https://eddb.io/system/ed-address/{this.system_address}') @@ -89,17 +92,38 @@ def system_url(system_name: str) -> str: def station_url(system_name: str, station_name: str) -> str: + """ + Construct an appropriate EDDB.IO URL for a station. + + Ignores `station_name` in favour of `this.station_marketid`. + + :param system_name: Name of the system the station is in. + :param station_name: **NOT USED** + :return: The URL, empty if no data was available to construct it. + """ if this.station_marketid: return requests.utils.requote_uri(f'https://eddb.io/station/market-id/{this.station_marketid}') return system_url(system_name) -def plugin_start3(plugin_dir): +def plugin_start3(plugin_dir: str) -> str: + """ + Start the plugin. + + :param plugin_dir: NAme of directory this was loaded from. + :return: Identifier string for this plugin. + """ return 'eddb' def plugin_app(parent: 'Tk'): + """ + Construct this plugin's main UI, if any. + + :param parent: The tk parent to place our widgets into. + :return: See PLUGINS.md#display + """ this.system_link = parent.children['system'] # system label in main window this.system = None this.system_address = None @@ -109,13 +133,34 @@ def plugin_app(parent: 'Tk'): this.station_link.configure(popup_copy=lambda x: x != STATION_UNDOCKED) -def prefs_changed(cmdr, is_beta): +def prefs_changed(cmdr: str, is_beta: bool) -> None: + """ + Update any saved configuration after Settings is closed. + + :param cmdr: Name of Commander. + :param is_beta: If game beta was detected. + """ # Do *NOT* set 'url' here, as it's set to a function that will call # through correctly. We don't want a static string. pass -def journal_entry(cmdr, is_beta, system, station, entry, state): +def journal_entry( # noqa: CCR001 + cmdr: str, is_beta: bool, system: str, station: str, + entry: MutableMapping[str, Any], + state: Mapping[str, Any] +): + """ + Handle a new Journal event. + + :param cmdr: Name of Commander. + :param is_beta: Whether game beta was detected. + :param system: Name of current tracked system. + :param station: Name of current tracked station location. + :param entry: The journal event. + :param state: `monitor.state` + :return: None if no error, else an error string. + """ should_return, new_entry = killswitch.check_killswitch('plugins.eddb.journal', entry) if should_return: # LANG: Journal Processing disabled due to an active killswitch @@ -179,7 +224,14 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): this.station_link.update_idletasks() -def cmdr_data(data: CAPIData, is_beta): +def cmdr_data(data: CAPIData, is_beta: bool) -> Optional[str]: + """ + Process new CAPI data. + + :param data: The latest merged CAPI data. + :param is_beta: Whether game beta was detected. + :return: Optional error string. + """ # Always store initially, even if we're not the *current* system provider. if not this.station_marketid and data['commander']['docked']: this.station_marketid = data['lastStarport']['id'] From 73c4bcfcc732747110852eee7e6794134b8650c4 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 15:57:09 +0000 Subject: [PATCH 32/41] plugins/eddb: flake8 & mypy now clean * Converted to `class This` paradigm. --- plugins/eddb.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/plugins/eddb.py b/plugins/eddb.py index b40d6855..e050df32 100644 --- a/plugins/eddb.py +++ b/plugins/eddb.py @@ -41,7 +41,7 @@ # # ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# # ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# -import sys +import tkinter from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Optional import requests @@ -55,23 +55,30 @@ from config import config if TYPE_CHECKING: from tkinter import Tk + def _(x: str) -> str: + return x logger = EDMCLogging.get_main_logger() -STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7 +class This: + """Holds module globals.""" -this: Any = sys.modules[__name__] # For holding module globals + STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7 -# Main window clicks -this.system_link: Optional[str] = None -this.system: Optional[str] = None -this.system_address: Optional[str] = None -this.system_population: Optional[int] = None -this.station_link: 'Optional[Tk]' = None -this.station: Optional[str] = None -this.station_marketid: Optional[int] = None -this.on_foot = False + def __init__(self) -> None: + # Main window clicks + self.system_link: tkinter.Widget + self.system: Optional[str] = None + self.system_address: Optional[str] = None + self.system_population: Optional[int] = None + self.station_link: tkinter.Widget + self.station: Optional[str] = None + self.station_marketid: Optional[int] = None + self.on_foot = False + + +this = This() def system_url(system_name: str) -> str: @@ -130,7 +137,7 @@ def plugin_app(parent: 'Tk'): this.station = None this.station_marketid = None # Frontier MarketID this.station_link = parent.children['station'] # station label in main window - this.station_link.configure(popup_copy=lambda x: x != STATION_UNDOCKED) + this.station_link['popup_copy'] = lambda x: x != this.STATION_UNDOCKED def prefs_changed(cmdr: str, is_beta: bool) -> None: @@ -213,7 +220,7 @@ def journal_entry( # noqa: CCR001 text = this.station if not text: if this.system_population is not None and this.system_population > 0: - text = STATION_UNDOCKED + text = this.STATION_UNDOCKED else: text = '' @@ -255,7 +262,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> Optional[str]: this.station_link['text'] = this.station elif data['lastStarport']['name'] and data['lastStarport']['name'] != "": - this.station_link['text'] = STATION_UNDOCKED + this.station_link['text'] = this.STATION_UNDOCKED else: this.station_link['text'] = '' @@ -263,3 +270,5 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> Optional[str]: # 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() + + return '' From ba81d95c1e10384393d2f87772a920416d83a962 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 16:12:58 +0000 Subject: [PATCH 33/41] plugins/edsm: Align comments and docstrings with plugins/eddb --- plugins/edsm.py | 94 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 43ecf3c6..130a794d 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -1,4 +1,4 @@ -"""System display and EDSM lookup.""" +"""Show EDSM data in display and handle lookups.""" # TODO: # 1) Re-factor EDSM API calls out of journal_entry() into own function. @@ -24,9 +24,9 @@ # Thus you **MUST** check if any imports you add in this file are only # referenced in this file (or only in any other core plugin), and if so... # -# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py` -# SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER -# INSTALLATION ON WINDOWS. +# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN +# `Build-exe-and-msi.py` SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN +# AN END-USER INSTALLATION ON WINDOWS. # # # ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# @@ -154,7 +154,13 @@ plEAADs= # Main window clicks def system_url(system_name: str) -> str: - """Get a URL for the current system.""" + """ + Construct an appropriate EDSM URL for the provided system. + + :param system_name: Will be overridden with `this.system_address` if that + is set. + :return: The URL, empty if no data was available to construct it. + """ if this.system_address: return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemID64={this.system_address}') @@ -165,7 +171,13 @@ def system_url(system_name: str) -> str: def station_url(system_name: str, station_name: str) -> str: - """Get a URL for the current station.""" + """ + Construct an appropriate EDSM URL for a station. + + :param system_name: Name of the system the station is in. + :param station_name: Name of the station. + :return: The URL, empty if no data was available to construct it. + """ if system_name and station_name: return requests.utils.requote_uri( f'https://www.edsm.net/en/system?systemName={system_name}&stationName={station_name}' @@ -186,7 +198,12 @@ def station_url(system_name: str, station_name: str) -> str: def plugin_start3(plugin_dir: str) -> str: - """Plugin setup hook.""" + """ + Start the plugin. + + :param plugin_dir: NAme of directory this was loaded from. + :return: Identifier string for this plugin. + """ # Can't be earlier since can only call PhotoImage after window is created this._IMG_KNOWN = tk.PhotoImage(data=IMG_KNOWN_B64) # green circle this._IMG_UNKNOWN = tk.PhotoImage(data=IMG_UNKNOWN_B64) # red circle @@ -225,7 +242,12 @@ def plugin_start3(plugin_dir: str) -> str: def plugin_app(parent: tk.Tk) -> None: - """Plugin UI setup.""" + """ + Construct this plugin's main UI, if any. + + :param parent: The tk parent to place our widgets into. + :return: See PLUGINS.md#display + """ this.system_link = parent.children['system'] # system label in main window this.system_link.bind_all('<>', update_status) this.station_link = parent.children['station'] # station label in main window @@ -246,7 +268,17 @@ def plugin_stop() -> None: def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: - """Plugin preferences setup hook.""" + """ + Plugin preferences setup hook. + + Any tkinter UI set up *must* be within an instance of `myNotebook.Frame`, + which is the return value of this function. + + :param parent: tkinter Widget to place items in. + :param cmdr: Name of Commander. + :param is_beta: Whether game beta was detected. + :return: An instance of `myNotebook.Frame`. + """ PADX = 10 # noqa: N806 BUTTONX = 12 # indent Checkbuttons and Radiobuttons # noqa: N806 PADY = 2 # close spacing # noqa: N806 @@ -313,7 +345,12 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: - """Commanders changed hook.""" + """ + 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) @@ -339,7 +376,7 @@ def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: def prefsvarchanged() -> None: - """Preferences screen closed hook.""" + """Handle the 'Send data to EDSM' tickbox changing state.""" to_set = tk.DISABLED if this.log.get(): to_set = this.log_button['state'] @@ -363,7 +400,12 @@ def set_prefs_ui_states(state: str) -> None: def prefs_changed(cmdr: str, is_beta: bool) -> None: - """Preferences changed hook.""" + """ + Handle any changes to Settings once the dialog is closed. + + :param cmdr: Name of Commander. + :param is_beta: Whether game beta was detected. + """ config.set('edsm_out', this.log.get()) if cmdr and not is_beta: @@ -427,15 +469,15 @@ def journal_entry( # noqa: C901, CCR001 cmdr: str, is_beta: bool, system: str, station: str, entry: MutableMapping[str, Any], state: Mapping[str, Any] ) -> str: """ - Process a Journal event. + Handle a new Journal event. - :param cmdr: - :param is_beta: - :param system: - :param station: - :param entry: - :param state: - :return: str - empty if no error, else error string. + :param cmdr: Name of Commander. + :param is_beta: Whether game beta was detected. + :param system: Name of current tracked system. + :param station: Name of current tracked station location. + :param entry: The journal event. + :param state: `monitor.state` + :return: None if no error, else an error string. """ should_return, new_entry = killswitch.check_killswitch('plugins.edsm.journal', entry, logger) if should_return: @@ -597,8 +639,14 @@ Queueing: {entry!r}''' # Update system data -def cmdr_data(data: CAPIData, is_beta: bool) -> None: - """CAPI Entry Hook.""" +def cmdr_data(data: CAPIData, is_beta: bool) -> Optional[str]: + """ + Process new CAPI data. + + :param data: The latest merged CAPI data. + :param is_beta: Whether game beta was detected. + :return: Optional error string. + """ system = data['lastSystem']['name'] # Always store initially, even if we're not the *current* system provider. @@ -640,6 +688,8 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> None: this.system_link['image'] = '' this.system_link.update_idletasks() + return '' + TARGET_URL = 'https://www.edsm.net/api-journal-v1' if 'edsm' in debug_senders: From a6f9a31fd96b35101f6e70945f9375a4d1a8c0bf Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 16:20:08 +0000 Subject: [PATCH 34/41] plugins/edsy: flake8 and mypy pass --- plugins/edsy.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/plugins/edsy.py b/plugins/edsy.py index df61683e..5ceb21d5 100644 --- a/plugins/edsy.py +++ b/plugins/edsy.py @@ -1,4 +1,4 @@ -# EDShipyard ship export +"""Export data for ED Shipyard.""" # ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# # ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# @@ -15,25 +15,41 @@ # Thus you **MUST** check if any imports you add in this file are only # referenced in this file (or only in any other core plugin), and if so... # -# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN `setup.py` -# SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER -# INSTALLATION ON WINDOWS. +# YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN +# `Build-exe-and-msi.py` SO AS TO ENSURE THE FILES ARE ACTUALLY PRESENT IN +# AN END-USER INSTALLATION ON WINDOWS. # # # ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# # ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# ! $# import base64 import gzip -import json import io +import json +from typing import Any, Mapping -def plugin_start3(plugin_dir): +def plugin_start3(plugin_dir: str) -> str: + """ + Start the plugin. + + :param plugin_dir: NAme of directory this was loaded from. + :return: Identifier string for this plugin. + """ return 'EDSY' + # Return a URL for the current ship -def shipyard_url(loadout, is_beta): - string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') # most compact representation +def shipyard_url(loadout: Mapping[str, Any], is_beta) -> bool | str: + """ + Construct a URL for ship loadout. + + :param loadout: + :param is_beta: + :return: + """ + # most compact representation + string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') if not string: return False @@ -41,4 +57,6 @@ def shipyard_url(loadout, is_beta): with gzip.GzipFile(fileobj=out, mode='w') as f: f.write(string) - return (is_beta and 'http://edsy.org/beta/#/I=' or 'http://edsy.org/#/I=') + base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D') + return ( + is_beta and 'http://edsy.org/beta/#/I=' or 'http://edsy.org/#/I=' + ) + base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D') From ccdfd9a4fafdb7a8d006258da3d4b4e3b516fd9a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 16:42:12 +0000 Subject: [PATCH 35/41] scripts/killswitch_test: One flake8 issue fixed --- scripts/killswitch_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/killswitch_test.py b/scripts/killswitch_test.py index 1b2f54a7..b6874e50 100644 --- a/scripts/killswitch_test.py +++ b/scripts/killswitch_test.py @@ -29,7 +29,7 @@ KNOWN_KILLSWITCH_NAMES: list[str] = [ 'plugins.eddb.journal.event.$event' ] -SPLIT_KNOWN_NAMES = list(map(lambda x: x.split('.'), KNOWN_KILLSWITCH_NAMES)) +SPLIT_KNOWN_NAMES = [x.split('.') for x in KNOWN_KILLSWITCH_NAMES] def match_exists(match: str) -> tuple[bool, str]: From 5c704e993277c23fe5e148b6fa27f59fa6788c2b Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 16:49:02 +0000 Subject: [PATCH 36/41] github/pr-checks: Remove stray `echo` from prior testing --- .github/workflows/pr-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 70c8dad1..78143675 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -85,7 +85,7 @@ jobs: # F63 - 'tests' checking # F7 - syntax errors # F82 - undefined checking - echo git diff --name-only --diff-filter=d -z "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | \ + git diff --name-only --diff-filter=d -z "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | \ grep -E -z -Z '\.py$' | \ xargs -0 flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # Can optionally add `--exit-zero` to the flake8 arguments so that From 486fb6b7e6bbcbfff63e6b9b4acddc7b1da30c02 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 16:50:21 +0000 Subject: [PATCH 37/41] github/pr-checks: We provide flake8 filenames, so lose the '.' --- .github/workflows/pr-checks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 78143675..8547540a 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -87,13 +87,13 @@ jobs: # F82 - undefined checking git diff --name-only --diff-filter=d -z "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | \ grep -E -z -Z '\.py$' | \ - xargs -0 flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + xargs -0 flake8 --count --select=E9,F63,F7,F82 --show-source --statistics # Can optionally add `--exit-zero` to the flake8 arguments so that # this doesn't fail the build. # explicitly ignore docstring errors (start with D) git diff --name-only --diff-filter=d -z "refs/remotes/${BASE_REPO_OWNER}/${BASE_REF}" -- | \ grep -E -z -Z '\.py$' | \ - xargs -0 flake8 . --count --statistics --extend-ignore D + xargs -0 flake8 --count --statistics --extend-ignore D #################################################################### #################################################################### From 4cea8fa34fb9aea5d6dc064551a5a908d9d5d162 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sun, 4 Dec 2022 16:55:45 +0000 Subject: [PATCH 38/41] github/push-checks: Align flake8 with what's working in pr-checks --- .github/workflows/push-checks.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push-checks.yml b/.github/workflows/push-checks.yml index a11234ed..e0c98897 100644 --- a/.github/workflows/push-checks.yml +++ b/.github/workflows/push-checks.yml @@ -51,9 +51,11 @@ jobs: ####################################################################### ####################################################################### - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + # 'Full' run, but ignoring docstring errors ####################################################################### + # explicitly ignore docstring errors (start with D) + # Can optionally add `--exit-zero` to the flake8 arguments so that git diff --name-only --diff-filter=d -z "$DATA" | \ grep -E -z -Z '\.py$' | \ - xargs -0 flake8 . --count --statistics + xargs -0 flake8 --count --statistics --extend-ignore D ####################################################################### From e4055530b47f40f07420e922bccfd7c4ed4788e5 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 5 Dec 2022 10:49:05 +0000 Subject: [PATCH 39/41] theme: The very start of actually using defined constants for themes --- theme.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/theme.py b/theme.py index de7ef696..2707ad42 100644 --- a/theme.py +++ b/theme.py @@ -123,6 +123,13 @@ elif sys.platform == 'linux': class _Theme(object): + # Enum ? Remember these are, probably, based on 'value' of a tk + # RadioButton set. Looking in prefs.py, they *appear* to be hard-coded + # there as well. + THEME_DEFAULT = 0 + THEME_DARK = 1 + THEME_TRANSPARENT = 2 + def __init__(self) -> None: self.active = None # Starts out with no theme self.minwidth: Optional[int] = None From c61f2357347e6edddf6661b2d316afa99183a57d Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 5 Dec 2022 10:55:53 +0000 Subject: [PATCH 40/41] loadout.py: Collapse the construction of output filename All the intermediate variables were more a "work in progress" thing. Tested as still working. --- loadout.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/loadout.py b/loadout.py index 805f245a..2e2061a2 100644 --- a/loadout.py +++ b/loadout.py @@ -49,19 +49,11 @@ def export(data: companion.CAPIData, requested_filename: Optional[str] = None) - query_time = config.get_int('querytime', default=int(time.time())) # Write - # - # When this is refactored into multi-line CHECK IT WORKS, avoiding the - # brainfart we had with dangling commas in commodity.py:export() !!! - # - output_path = pathlib.Path(config.get_str('outdir')) - output_filename = pathlib.Path( - ship + '.' + time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(query_time)) + '.txt' - ) - # Can't re-use `filename`, different type - file_name = output_path / output_filename - # - # When this is refactored into multi-line CHECK IT WORKS, avoiding the - # brainfart we had with dangling commas in commodity.py:export() !!! - # - with open(file_name, 'wt') as h: + + with open( + pathlib.Path(config.get_str('outdir')) / pathlib.Path( + ship + '.' + time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(query_time)) + '.txt' + ), + 'wt' + ) as h: h.write(string) From ab979ed043292b39552becee0d62acfd60361a07 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 5 Dec 2022 10:58:16 +0000 Subject: [PATCH 41/41] ttkHyperlinkLabel: Use `Any` where it's appropriate in function signatures --- ttkHyperlinkLabel.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index e29caebf..8abc0d00 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -33,8 +33,7 @@ if TYPE_CHECKING: class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object): # type: ignore """Clickable label for HTTP links.""" - # NB: do **NOT** try `**kw: Dict`, it causes more trouble than it's worth. - def __init__(self, master: Optional[tk.Tk] = None, **kw) -> None: + def __init__(self, master: Optional[tk.Tk] = 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 @@ -66,9 +65,8 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) text=kw.get('text'), font=kw.get('font', ttk.Style().lookup('TLabel', 'font'))) - # NB: do **NOT** try `**kw: Dict`, it causes more trouble than it's worth. def configure( # noqa: CCR001 - self, cnf: dict[str, Any] | None = None, **kw + self, cnf: dict[str, Any] | None = None, **kw: Any ) -> dict[str, tuple[str, str, str, Any, Any]] | None: """Change cursor and appearance depending on state and text.""" # This class' state @@ -104,7 +102,7 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object) return super(HyperlinkLabel, self).configure(cnf, **kw) - def __setitem__(self, key: str, value) -> None: + def __setitem__(self, key: str, value: Any) -> None: """ Allow for dict member style setting of options.