mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-18 18:07:37 +03:00
Merge pull request #1740 from EDCD/fix/1723/flake8-everything
Get everything passing flake8 checks
This commit is contained in:
commit
3ed568e544
8
.github/workflows/pr-checks.yml
vendored
8
.github/workflows/pr-checks.yml
vendored
@ -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
|
||||
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
|
||||
####################################################################
|
||||
|
||||
####################################################################
|
||||
|
21
.github/workflows/push-checks.yml
vendored
21
.github/workflows/push-checks.yml
vendored
@ -40,7 +40,22 @@ 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
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
git diff "$DATA" | flake8 . --count --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
|
||||
#######################################################################
|
||||
|
||||
#######################################################################
|
||||
# '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 --extend-ignore D
|
||||
#######################################################################
|
||||
|
@ -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('<Button-1>', 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)
|
||||
@ -558,7 +560,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 +603,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 +726,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 +735,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 +1383,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 +1434,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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
47
loadout.py
47
loadout.py
@ -1,48 +1,59 @@
|
||||
# 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
|
||||
|
||||
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))))
|
||||
#
|
||||
# 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(
|
||||
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)
|
||||
|
@ -1,14 +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
|
||||
from typing import Optional
|
||||
|
||||
# Can't do this with styles on OSX - http://www.tkdocs.com/tutorial/styles.html#whydifficult
|
||||
if sys.platform == 'darwin':
|
||||
@ -22,8 +27,9 @@ 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()
|
||||
@ -46,9 +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)
|
||||
@ -63,8 +73,9 @@ 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)
|
||||
@ -74,9 +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)
|
||||
@ -84,9 +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)
|
||||
@ -96,9 +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)
|
||||
@ -113,9 +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)
|
||||
@ -126,9 +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)
|
||||
@ -139,7 +155,8 @@ 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):
|
||||
if sys.platform == 'darwin':
|
||||
|
187
plug.py
187
plug.py
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Plugin hooks for EDMC - Ian Norton, Jonathan Harris
|
||||
"""
|
||||
"""Plugin API."""
|
||||
import copy
|
||||
import importlib
|
||||
import logging
|
||||
@ -9,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
|
||||
@ -21,34 +20,48 @@ 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
|
||||
: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.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}"')
|
||||
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
|
||||
@ -58,23 +71,25 @@ 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:
|
||||
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
|
||||
Get a function from a plugin.
|
||||
|
||||
:param funcname:
|
||||
:returns: The function, or None if it isn't implemented.
|
||||
"""
|
||||
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.
|
||||
|
||||
:param parent: the parent frame for this entry.
|
||||
:returns: None, a tk Widget, or a pair of tk.Widgets
|
||||
"""
|
||||
@ -84,19 +99,29 @@ 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:
|
||||
|
||||
except Exception:
|
||||
logger.exception(f'Failed for Plugin "{self.name}"')
|
||||
|
||||
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.
|
||||
|
||||
: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.
|
||||
@ -110,16 +135,14 @@ 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
|
||||
"""
|
||||
last_error['root'] = master
|
||||
def load_plugins(master: tk.Frame) -> None: # noqa: CCR001
|
||||
"""Find and load all plugins."""
|
||||
last_error.root = master
|
||||
|
||||
internal = []
|
||||
for name in sorted(os.listdir(config.internal_plugin_dir_path)):
|
||||
@ -128,7 +151,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()))
|
||||
|
||||
@ -137,8 +160,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'):
|
||||
@ -155,15 +180,16 @@ 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()))
|
||||
|
||||
|
||||
def provides(fn_name):
|
||||
def provides(fn_name: str) -> List[str]:
|
||||
"""
|
||||
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
|
||||
@ -171,9 +197,12 @@ 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
|
||||
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:
|
||||
@ -182,17 +211,24 @@ 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.
|
||||
|
||||
If your plugin uses threads then stop and join() them before returning.
|
||||
.. versionadded:: 2.3.7
|
||||
"""
|
||||
@ -204,7 +240,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')
|
||||
@ -212,9 +248,10 @@ 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 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.
|
||||
@ -224,13 +261,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):
|
||||
def notify_prefs_changed(cmdr: str, is_beta: bool) -> None:
|
||||
"""
|
||||
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.
|
||||
@ -242,13 +280,18 @@ 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):
|
||||
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.
|
||||
|
||||
: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
|
||||
@ -268,21 +311,25 @@ 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):
|
||||
def notify_journal_entry_cqc(
|
||||
cmdr: str, is_beta: bool,
|
||||
entry: MutableMapping[str, Any],
|
||||
state: Mapping[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
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')
|
||||
@ -298,9 +345,13 @@ 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.
|
||||
|
||||
: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
|
||||
@ -314,14 +365,18 @@ 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):
|
||||
def notify_newdata(
|
||||
data: companion.CAPIData,
|
||||
is_beta: bool
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
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)
|
||||
@ -333,12 +388,12 @@ 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
|
||||
|
||||
|
||||
def show_error(err):
|
||||
def show_error(err: str) -> None:
|
||||
"""
|
||||
Display an error message in the status line of the main window.
|
||||
|
||||
@ -350,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('<<PluginError>>', when="tail")
|
||||
if err and last_error.root:
|
||||
last_error.msg = str(err)
|
||||
last_error.root.event_generate('<<PluginError>>', when="tail")
|
||||
|
115
plugins/eddb.py
115
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
|
||||
import tkinter
|
||||
from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Optional
|
||||
|
||||
import requests
|
||||
|
||||
@ -59,26 +55,40 @@ from config import config
|
||||
if TYPE_CHECKING:
|
||||
from tkinter import Tk
|
||||
|
||||
def _(x: str) -> str:
|
||||
return x
|
||||
|
||||
logger = EDMCLogging.get_main_logger()
|
||||
|
||||
|
||||
class This:
|
||||
"""Holds module globals."""
|
||||
|
||||
STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7
|
||||
|
||||
this: Any = sys.modules[__name__] # For holding module globals
|
||||
|
||||
def __init__(self) -> None:
|
||||
# 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
|
||||
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:
|
||||
"""
|
||||
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,33 +99,75 @@ 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
|
||||
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, 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
|
||||
@ -168,7 +220,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
||||
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 = ''
|
||||
@ -179,7 +231,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']
|
||||
@ -203,7 +262,7 @@ def cmdr_data(data: CAPIData, is_beta):
|
||||
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'] = ''
|
||||
@ -211,3 +270,5 @@ def cmdr_data(data: CAPIData, is_beta):
|
||||
# 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 ''
|
||||
|
@ -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('<<EDSMStatus>>', 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:
|
||||
|
@ -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')
|
||||
|
29
protocol.py
29
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
|
||||
|
||||
@ -107,11 +106,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
|
||||
@ -196,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.
|
||||
|
||||
@ -215,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
|
||||
@ -236,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 <https://docs.python.org/3/library/ctypes.html#fundamental-data-types>
|
||||
return c_long(0)
|
||||
|
||||
return c_long(1) # This is an utter guess -Ath
|
||||
|
||||
class WindowsProtocolHandler(GenericProtocolHandler):
|
||||
"""
|
||||
@ -349,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')
|
||||
@ -389,7 +394,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:
|
||||
@ -411,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
|
||||
|
||||
|
@ -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]:
|
||||
|
39
shipyard.py
39
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']
|
||||
))
|
||||
|
83
td.py
83
td.py
@ -1,12 +1,12 @@
|
||||
# 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
|
||||
@ -19,52 +19,47 @@ stockbracketmap = { 0: '-',
|
||||
2: 'M',
|
||||
3: 'H', }
|
||||
|
||||
def export(data):
|
||||
|
||||
querytime = config.get_int('querytime', default=int(time.time()))
|
||||
|
||||
#
|
||||
# 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() !!!
|
||||
#
|
||||
|
||||
timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.strptime(data['timestamp'], '%Y-%m-%dT%H:%M:%SZ'))
|
||||
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"
|
||||
|
||||
# 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 = 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# <item name> <sellCR> <buyCR> <demand> <stock> <timestamp>\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'))
|
||||
h.write('#! trade.py import -\n'.encode('utf-8'))
|
||||
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# <item name> <sellCR> <buyCR> <demand> <stock> <timestamp>\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)
|
||||
|
||||
for category in sorted(bycategory):
|
||||
h.write(' + {}\n'.format(category).encode('utf-8'))
|
||||
timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.strptime(data['timestamp'], '%Y-%m-%dT%H:%M:%SZ'))
|
||||
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'))
|
||||
|
||||
h.close()
|
||||
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')
|
||||
)
|
||||
|
151
theme.py
151
theme.py
@ -1,23 +1,27 @@
|
||||
#
|
||||
# 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 typing import TYPE_CHECKING, Callable, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from config import config
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
from EDMCLogging import get_main_logger
|
||||
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)
|
||||
@ -65,6 +69,8 @@ elif sys.platform == 'linux':
|
||||
MWM_DECOR_MAXIMIZE = 1 << 6
|
||||
|
||||
class MotifWmHints(Structure):
|
||||
"""MotifWmHints structure."""
|
||||
|
||||
_fields_ = [
|
||||
('flags', c_ulong),
|
||||
('functions', c_ulong),
|
||||
@ -99,33 +105,44 @@ 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,
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
class _Theme(object):
|
||||
|
||||
def __init__(self):
|
||||
# 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 = 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):
|
||||
# Note widget and children for later application of a theme. Note if the widget has explicit fg or bg attributes.
|
||||
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
|
||||
if not self.defaults:
|
||||
# Can't initialise this til window is created # Windows, MacOS
|
||||
@ -160,7 +177,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
|
||||
@ -184,15 +204,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('<Button-1>', command)
|
||||
widget.bind('<Enter>', lambda e: self._enter(e, image))
|
||||
widget.bind('<Leave>', 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:
|
||||
@ -209,7 +231,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:
|
||||
@ -226,7 +248,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')
|
||||
@ -245,12 +267,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:
|
||||
@ -271,10 +294,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):
|
||||
@ -282,73 +306,76 @@ 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'])
|
||||
attribs = self.widgets.get(widget, [])
|
||||
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: 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['foreground'] = self.current['foreground']
|
||||
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background=self.current['background'])
|
||||
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']),
|
||||
widget['foreground'] = self.current['highlight']
|
||||
if 'insertbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(insertbackground=self.current['foreground']),
|
||||
widget['insertbackground'] = self.current['foreground']
|
||||
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background=self.current['background'])
|
||||
widget['background'] = self.current['background']
|
||||
if 'highlightbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(highlightbackground=self.current['background'])
|
||||
widget['highlightbackground'] = self.current['background']
|
||||
|
||||
if 'font' not in attribs:
|
||||
widget.configure(font=self.current['font'])
|
||||
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'],
|
||||
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'],
|
||||
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'])
|
||||
widget['highlightbackground'] = self.current['background']
|
||||
|
||||
if 'font' not in attribs:
|
||||
widget.configure(font=self.current['font'])
|
||||
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']),
|
||||
widget['foreground'] = self.current['foreground']
|
||||
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background=self.current['background'])
|
||||
widget['background'] = self.current['background']
|
||||
|
||||
if 'font' not in attribs:
|
||||
widget.configure(font=self.current['font'])
|
||||
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'],
|
||||
highlightbackground=self.current['disabledforeground'])
|
||||
widget['background'] = self.current['background']
|
||||
widget['highlightbackground'] = self.current['disabledforeground']
|
||||
|
||||
except Exception:
|
||||
logger.exception(f'Plugin widget issue ? {widget=}')
|
||||
|
||||
# Apply configured theme
|
||||
|
||||
def apply(self, root):
|
||||
def apply(self, root: tk.Tk) -> None: # noqa: CCR001
|
||||
|
||||
theme = config.get_int('theme')
|
||||
self._colors(root, theme)
|
||||
@ -390,16 +417,16 @@ 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.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
|
||||
|
@ -1,26 +1,39 @@
|
||||
"""
|
||||
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
|
||||
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
|
||||
#
|
||||
# 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
|
||||
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):
|
||||
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
|
||||
@ -33,8 +46,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('<Button-1>', self._click)
|
||||
|
||||
@ -51,8 +65,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')))
|
||||
|
||||
# Change cursor and appearance depending on state and text
|
||||
def configure(self, cnf=None, **kw):
|
||||
def configure( # noqa: CCR001
|
||||
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
|
||||
for thing in ['url', 'popup_copy', 'underline']:
|
||||
if thing in kw:
|
||||
@ -71,7 +87,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
|
||||
|
||||
@ -84,41 +100,53 @@ 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):
|
||||
def __setitem__(self, key: str, value: Any) -> 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):
|
||||
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)
|
||||
|
||||
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: str) -> 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 +156,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)
|
||||
(value, typ) = QueryValueEx(hkey, None)
|
||||
hkey = OpenKeyEx(HKEY_CLASSES_ROOT, rf'{cls}\shell\open\command')
|
||||
(value, typ) = QueryValueEx(hkey, '')
|
||||
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)
|
||||
|
127
update.py
127
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 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,36 +41,38 @@ 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
|
||||
"""
|
||||
if not config.shutting_down:
|
||||
"""Receive (Win)Sparkle shutdown request and send it to parent."""
|
||||
if not config.shutting_down and self.root:
|
||||
self.root.event_generate('<<Quit>>', 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: 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
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
|
||||
if self.use_internal():
|
||||
return
|
||||
@ -71,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())
|
||||
@ -84,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)
|
||||
@ -93,7 +101,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 +109,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,11 +137,8 @@ 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.daemon = True
|
||||
@ -142,65 +152,78 @@ 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
|
||||
|
||||
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')
|
||||
# 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:
|
||||
continue
|
||||
|
||||
# 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,
|
||||
items[sv] = EDMCVersion(
|
||||
version=str(ver), # sv might have mangled version
|
||||
title=item.find('title').text, # type: ignore
|
||||
sv=sv
|
||||
)
|
||||
|
||||
# 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:
|
||||
"""
|
||||
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'
|
||||
if newversion and self.root:
|
||||
status = self.root.nametowidget(f'.{appname.lower()}.status')
|
||||
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 +231,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
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user