From 8b5e5e73dec3297504cf3544c690343af9bdcd64 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 15 Sep 2020 10:24:38 +0100 Subject: [PATCH 01/71] Locale: More detailed login at startup around changes As we might run into some special cases with users we need to log in more detail what the locale is around our changes. NB: Also contains some misc. PyCharm-enacted formatting changes, white space, wrapping etc. --- EDMarketConnector.py | 115 ++++++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 4d25fe17..a3e46aac 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -23,6 +23,7 @@ if __name__ == "__main__": if getattr(sys, 'frozen', False): # By default py2exe tries to write log to dirname(sys.executable) which fails when installed import tempfile + # unbuffered not allowed for text in python3, so use `1 for line buffering sys.stdout = sys.stderr = open(join(tempfile.gettempdir(), f'{appname}.log'), mode='wt', buffering=1) @@ -45,6 +46,7 @@ if __debug__: if platform != 'win32': import pdb import signal + signal.signal(signal.SIGTERM, lambda sig, frame: pdb.Pdb().set_trace(frame)) import companion @@ -61,7 +63,6 @@ from protocol import protocolhandler from dashboard import dashboard from theme import theme - SERVER_RETRY = 5 # retry pause for Companion servers [s] SHIPYARD_HTML_TEMPLATE = """ @@ -81,7 +82,6 @@ SHIPYARD_HTML_TEMPLATE = """ class AppWindow(object): - # Tkinter Event types EVENT_KEYPRESS = 2 EVENT_BUTTON = 4 @@ -104,10 +104,14 @@ class AppWindow(object): if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') else: - self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501 - self.theme_icon = tk.PhotoImage(data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501 - self.theme_minimize = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 - self.theme_close = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 + self.w.tk.call('wm', 'iconphoto', self.w, '-default', + tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501 + self.theme_icon = tk.PhotoImage( + data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501 + self.theme_minimize = tk.BitmapImage( + data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 + self.theme_close = tk.BitmapImage( + data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) @@ -152,7 +156,8 @@ class AppWindow(object): row = frame.grid_size()[1] self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) - theme.register_alternate((self.button, self.theme_button, self.theme_button), {'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501 + theme.register_alternate((self.button, self.theme_button, self.theme_button), + {'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501 self.status.grid(columnspan=2, sticky=tk.EW) self.button.bind('', self.getandsend) theme.button_bind(self.theme_button, self.getandsend) @@ -291,6 +296,7 @@ class AppWindow(object): # Check that the titlebar will be at least partly on screen import ctypes from ctypes.wintypes import POINT + # https://msdn.microsoft.com/en-us/library/dd145064 MONITOR_DEFAULTTONULL = 0 # noqa: N806 if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16), @@ -322,6 +328,7 @@ class AppWindow(object): # Load updater after UI creation (for WinSparkle) import update + if getattr(sys, 'frozen', False): # Running in frozen .exe, so use (Win)Sparkle self.updater = update.Updater(tkroot=self.w, provider='external') @@ -453,7 +460,7 @@ class AppWindow(object): if not retrying: if time() < self.holdofftime: # Was invoked by key while in cooldown self.status['text'] = '' - if play_sound and (self.holdofftime-time()) < companion.holdoff*0.75: + if play_sound and (self.holdofftime - time()) < companion.holdoff * 0.75: hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats return elif play_sound: @@ -493,7 +500,7 @@ class AppWindow(object): if isdir('dump'): with open('dump/{system}{station}.{timestamp}.json'.format( system=data['lastSystem']['name'], - station=data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '', + station=data['commander'].get('docked') and '.' + data['lastStarport']['name'] or '', timestamp=strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wb') as h: h.write(json.dumps(data, ensure_ascii=False, @@ -525,7 +532,7 @@ class AppWindow(object): self.status['text'] = _("You're not docked at a station!") play_bad = True # Ignore possibly missing shipyard info - elif (config.getint('output') & config.OUT_MKT_EDDN)\ + elif (config.getint('output') & config.OUT_MKT_EDDN) \ and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): if not self.status['text']: self.status['text'] = _("Station doesn't have anything!") @@ -585,12 +592,12 @@ class AppWindow(object): if not data['commander'].get('docked'): # might have un-docked while we were waiting for retry in which case station data is unreliable pass - elif (data.get('lastSystem', {}).get('name') == monitor.system and + elif (data.get('lastSystem', {}).get('name') == monitor.system and data.get('lastStarport', {}).get('name') == monitor.station and data.get('lastStarport', {}).get('ships', {}).get('shipyard_list')): self.eddn.export_shipyard(data, monitor.is_beta) elif tries > 1: # bogus data - retry - self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries-1)) + self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries - 1)) except Exception: pass @@ -600,11 +607,11 @@ class AppWindow(object): def crewroletext(role): # Return translated crew role. Needs to be dynamic to allow for changing language. return { - None: '', - 'Idle': '', + None : '', + 'Idle' : '', 'FighterCon': _('Fighter'), # Multicrew role - 'FireCon': _('Gunner'), # Multicrew role - 'FlightCon': _('Helm'), # Multicrew role + 'FireCon' : _('Gunner'), # Multicrew role + 'FlightCon' : _('Helm'), # Multicrew role }.get(role, role) while True: @@ -626,8 +633,8 @@ class AppWindow(object): self.ship_label['text'] = _('Ship') + ':' # Main window self.ship.configure( text=monitor.state['ShipName'] - or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType']) - or '', + or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType']) + or '', url=self.shipyard_url) else: self.cmdr['text'] = '' @@ -673,7 +680,7 @@ class AppWindow(object): logger.info("Can't start Status monitoring") # Export loadout - if entry['event'] == 'Loadout' and not monitor.state['Captain']\ + if entry['event'] == 'Loadout' and not monitor.state['Captain'] \ and config.getint('output') & config.OUT_SHIP: monitor.export_ship() @@ -690,10 +697,10 @@ class AppWindow(object): hotkeymgr.play_bad() # Auto-Update after docking, but not if auth callback is pending - if entry['event'] in ('StartUp', 'Location', 'Docked')\ - and monitor.station\ - and not config.getint('output') & config.OUT_MKT_MANUAL\ - and config.getint('output') & config.OUT_STATION_ANY\ + if entry['event'] in ('StartUp', 'Location', 'Docked') \ + and monitor.station \ + and not config.getint('output') & config.OUT_MKT_MANUAL \ + and config.getint('output') & config.OUT_STATION_ANY \ and companion.session.state != companion.Session.STATE_AUTH: self.w.after(int(SERVER_RETRY * 1000), self.getandsend) @@ -760,10 +767,10 @@ class AppWindow(object): return f'file://localhost/{file_name}' def system_url(self, system): - return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system) + return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system) def station_url(self, station): - return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station) + return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station) def cooldown(self): if time() < self.holdofftime: @@ -837,7 +844,7 @@ class AppWindow(object): ############################################################ # version - ttk.Label(frame).grid(row=row, column=0) # spacer + ttk.Label(frame).grid(row=row, column=0) # spacer row += 1 self.appversion_label = tk.Label(frame, text=appversion) self.appversion_label.grid(row=row, column=0, sticky=tk.E) @@ -855,7 +862,7 @@ class AppWindow(object): ############################################################ # - ttk.Label(frame).grid(row=row, column=0) # spacer + ttk.Label(frame).grid(row=row, column=0) # spacer row += 1 self.copyright = tk.Label(frame, text=copyright) self.copyright.grid(row=row, columnspan=3, sticky=tk.EW) @@ -864,7 +871,7 @@ class AppWindow(object): ############################################################ # OK button to close the window - ttk.Label(frame).grid(row=row, column=0) # spacer + ttk.Label(frame).grid(row=row, column=0) # spacer row += 1 button = ttk.Button(frame, text=_('OK'), command=self.apply) button.grid(row=row, column=2, sticky=tk.E) @@ -895,7 +902,7 @@ class AppWindow(object): last_system: str = data.get("lastSystem", {}).get("name", "Unknown") last_starport: str = '' if data['commander'].get('docked'): - last_starport = '.'+data.get('lastStarport', {}).get('name', 'Unknown') + last_starport = '.' + data.get('lastStarport', {}).get('name', 'Unknown') timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime()) f = tkinter.filedialog.asksaveasfilename(parent=self.w, defaultextension=default_extension, @@ -971,6 +978,7 @@ def enforce_single_instance() -> None: if platform == 'win32': import ctypes from ctypes.wintypes import HWND, LPWSTR, LPCWSTR, INT, BOOL, LPARAM + EnumWindows = ctypes.windll.user32.EnumWindows # noqa: N806 GetClassName = ctypes.windll.user32.GetClassNameW # noqa: N806 GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int] # noqa: N806 @@ -1004,9 +1012,9 @@ def enforce_single_instance() -> None: def enumwindowsproc(window_handle, l_param): # class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576 cls = ctypes.create_unicode_buffer(257) - if GetClassName(window_handle, cls, 257)\ - and cls.value == 'TkTopLevel'\ - and window_title(window_handle) == applongname\ + if GetClassName(window_handle, cls, 257) \ + and cls.value == 'TkTopLevel' \ + and window_title(window_handle) == applongname \ and GetProcessHandleFromHwnd(window_handle): # If GetProcessHandleFromHwnd succeeds then the app is already running as this user if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler.redirect): @@ -1028,38 +1036,52 @@ def test_logging(): logger.debug('Test from EDMarketConnector.py top-level test_logging()') -# Run the app -if __name__ == "__main__": - enforce_single_instance() - - from EDMCLogging import logger - logger.info(f'Startup v{appversion} : Running on Python v{sys.version}') - logger.debug(f'''Platform: {sys.platform} -argv[0]: {sys.argv[0]} -exec_prefix: {sys.exec_prefix} -executable: {sys.executable} -sys.path: {sys.path} +def log_locale(prefix: str) -> None: + logger.debug(f'''Locale: {prefix} Locale LC_COLLATE: {locale.getlocale(locale.LC_COLLATE)} Locale LC_CTYPE: {locale.getlocale(locale.LC_CTYPE)} Locale LC_MONETARY: {locale.getlocale(locale.LC_MONETARY)} Locale LC_NUMERIC: {locale.getlocale(locale.LC_NUMERIC)} Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}''' -) + ) + + +# Run the app +if __name__ == "__main__": + enforce_single_instance() + + from EDMCLogging import logger + + logger.info(f'Startup v{appversion} : Running on Python v{sys.version}') + logger.debug(f'''Platform: {sys.platform} +argv[0]: {sys.argv[0]} +exec_prefix: {sys.exec_prefix} +executable: {sys.executable} +sys.path: {sys.path}''' + ) # Change locale to a utf8 one - # First make sure the local is actually set as per locale's idea of defaults + # Log what we have at startup + log_locale('Initial Locale') + # Make sure the local is actually set as per locale's idea of defaults locale.setlocale(locale.LC_ALL, '') + log_locale('After LC_ALL defaults set') # Now find out the current locale, mostly the language locale_startup = locale.getlocale(locale.LC_ALL) + logger.debug(f'Locale LC_ALL: {locale_startup}') # Now set that same language, but utf8 encoding (it was probably cp1252 # or equivalent for other languages). locale.setlocale(locale.LC_ALL, (locale_startup[0], 'utf8')) + log_locale('After switching to utf8 encoding (same language)') # TODO: unittests in place of these # logger.debug('Test from __main__') # test_logging() + class A(object): + class B(object): + def __init__(self): logger.debug('A call from A.B.__init__') @@ -1102,7 +1124,8 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}''' # Now the string should match, so try translation popup_text = _(popup_text) # And substitute in the other words. - popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled') + popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), + DISABLED='.disabled') # And now we do need these to be actual \r\n popup_text = popup_text.replace('\\n', '\n') popup_text = popup_text.replace('\\r', '\r') From c12c739c11af5e55750b4e9dd7cc438a350e4312 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 15 Sep 2020 14:15:07 +0100 Subject: [PATCH 02/71] companion.py: Full pass to 100% pass flake8 * docstrings added throughout. * .format() all now f-strings. * Type annotations added throughout. * White space tweaked. --- companion.py | 326 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 192 insertions(+), 134 deletions(-) diff --git a/companion.py b/companion.py index 947ec170..b6396827 100644 --- a/companion.py +++ b/companion.py @@ -1,31 +1,38 @@ -from builtins import str -from builtins import range -from builtins import object +""" +Handle use of Frontier's Companion API (CAPI) service. + +Deals with initiating authentication for, and use of, CAPI. +Some associated code is in protocol.py which creates and handles the edmc:// +protocol used for the callback. +""" + import base64 import csv -import requests -from typing import TYPE_CHECKING - -# TODO: see https://github.com/EDCD/EDMarketConnector/issues/569 -from http.cookiejar import LWPCookieJar # noqa: F401 - No longer needed but retained in case plugins use it -from email.utils import parsedate import hashlib +import logging import numbers import os -from os.path import join import random import time import urllib.parse import webbrowser +from builtins import object, range, str +from email.utils import parsedate +# TODO: see https://github.com/EDCD/EDMarketConnector/issues/569 +from http.cookiejar import LWPCookieJar # noqa: F401 - No longer needed but retained in case plugins use it +from os.path import join +from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Union + +import requests from config import appname, appversion, config from protocol import protocolhandler -import logging + logger = logging.getLogger(appname) if TYPE_CHECKING: - _ = lambda x: x # noqa # to make flake8 stop complaining that the hacked in _ method doesnt exist + _ = lambda x: x # noqa: E731 # to make flake8 stop complaining that the hacked in _ method doesnt exist holdoff = 60 # be nice @@ -35,83 +42,89 @@ auth_timeout = 30 # timeout for initial auth # Currently the "Elite Dangerous Market Connector (EDCD/Athanasius)" one in # Athanasius' Frontier account # Obtain from https://auth.frontierstore.net/client/signup -CLIENT_ID = os.getenv('CLIENT_ID') or 'fb88d428-9110-475f-a3d2-dc151c2b9c7a' +CLIENT_ID = os.getenv('CLIENT_ID') or 'fb88d428-9110-475f-a3d2-dc151c2b9c7a' SERVER_AUTH = 'https://auth.frontierstore.net' -URL_AUTH = '/auth' +URL_AUTH = '/auth' URL_TOKEN = '/token' -USER_AGENT = 'EDCD-{}-{}'.format(appname, appversion) +USER_AGENT = f'EDCD-{appname}-{appversion}' SERVER_LIVE = 'https://companion.orerve.net' SERVER_BETA = 'https://pts-companion.orerve.net' -URL_QUERY = '/profile' -URL_MARKET = '/market' -URL_SHIPYARD= '/shipyard' +URL_QUERY = '/profile' +URL_MARKET = '/market' +URL_SHIPYARD = '/shipyard' # Map values reported by the Companion interface to names displayed in-game # May be imported by plugins category_map = { - 'Narcotics' : 'Legal Drugs', - 'Slaves' : 'Slavery', - 'Waste ' : 'Waste', - 'NonMarketable' : False, # Don't appear in the in-game market so don't report + 'Narcotics': 'Legal Drugs', + 'Slaves': 'Slavery', + 'Waste ': 'Waste', + 'NonMarketable': False, # Don't appear in the in-game market so don't report } commodity_map = {} ship_map = { - 'adder' : 'Adder', - 'anaconda' : 'Anaconda', - 'asp' : 'Asp Explorer', - 'asp_scout' : 'Asp Scout', - 'belugaliner' : 'Beluga Liner', - 'cobramkiii' : 'Cobra MkIII', - 'cobramkiv' : 'Cobra MkIV', - 'clipper' : 'Panther Clipper', - 'cutter' : 'Imperial Cutter', - 'diamondback' : 'Diamondback Scout', - 'diamondbackxl' : 'Diamondback Explorer', - 'dolphin' : 'Dolphin', - 'eagle' : 'Eagle', - 'empire_courier' : 'Imperial Courier', - 'empire_eagle' : 'Imperial Eagle', - 'empire_fighter' : 'Imperial Fighter', - 'empire_trader' : 'Imperial Clipper', - 'federation_corvette' : 'Federal Corvette', - 'federation_dropship' : 'Federal Dropship', - 'federation_dropship_mkii' : 'Federal Assault Ship', - 'federation_gunship' : 'Federal Gunship', - 'federation_fighter' : 'F63 Condor', - 'ferdelance' : 'Fer-de-Lance', - 'hauler' : 'Hauler', - 'independant_trader' : 'Keelback', - 'independent_fighter' : 'Taipan Fighter', - 'krait_mkii' : 'Krait MkII', - 'krait_light' : 'Krait Phantom', - 'mamba' : 'Mamba', - 'orca' : 'Orca', - 'python' : 'Python', - 'scout' : 'Taipan Fighter', - 'sidewinder' : 'Sidewinder', - 'testbuggy' : 'Scarab', - 'type6' : 'Type-6 Transporter', - 'type7' : 'Type-7 Transporter', - 'type9' : 'Type-9 Heavy', - 'type9_military' : 'Type-10 Defender', - 'typex' : 'Alliance Chieftain', - 'typex_2' : 'Alliance Crusader', - 'typex_3' : 'Alliance Challenger', - 'viper' : 'Viper MkIII', - 'viper_mkiv' : 'Viper MkIV', - 'vulture' : 'Vulture', + 'adder': 'Adder', + 'anaconda': 'Anaconda', + 'asp': 'Asp Explorer', + 'asp_scout': 'Asp Scout', + 'belugaliner': 'Beluga Liner', + 'cobramkiii': 'Cobra MkIII', + 'cobramkiv': 'Cobra MkIV', + 'clipper': 'Panther Clipper', + 'cutter': 'Imperial Cutter', + 'diamondback': 'Diamondback Scout', + 'diamondbackxl': 'Diamondback Explorer', + 'dolphin': 'Dolphin', + 'eagle': 'Eagle', + 'empire_courier': 'Imperial Courier', + 'empire_eagle': 'Imperial Eagle', + 'empire_fighter': 'Imperial Fighter', + 'empire_trader': 'Imperial Clipper', + 'federation_corvette': 'Federal Corvette', + 'federation_dropship': 'Federal Dropship', + 'federation_dropship_mkii': 'Federal Assault Ship', + 'federation_gunship': 'Federal Gunship', + 'federation_fighter': 'F63 Condor', + 'ferdelance': 'Fer-de-Lance', + 'hauler': 'Hauler', + 'independant_trader': 'Keelback', + 'independent_fighter': 'Taipan Fighter', + 'krait_mkii': 'Krait MkII', + 'krait_light': 'Krait Phantom', + 'mamba': 'Mamba', + 'orca': 'Orca', + 'python': 'Python', + 'scout': 'Taipan Fighter', + 'sidewinder': 'Sidewinder', + 'testbuggy': 'Scarab', + 'type6': 'Type-6 Transporter', + 'type7': 'Type-7 Transporter', + 'type9': 'Type-9 Heavy', + 'type9_military': 'Type-10 Defender', + 'typex': 'Alliance Chieftain', + 'typex_2': 'Alliance Crusader', + 'typex_3': 'Alliance Challenger', + 'viper': 'Viper MkIII', + 'viper_mkiv': 'Viper MkIV', + 'vulture': 'Vulture', } -# Companion API sometimes returns an array as a json array, sometimes as a json object indexed by "int". -# This seems to depend on whether the there are 'gaps' in the Cmdr's data - i.e. whether the array is sparse. -# In practice these arrays aren't very sparse so just convert them to lists with any 'gaps' holding None. -def listify(thing): +def listify(thing: Union[List, Dict]) -> List: + """ + Convert actual JSON array or int-indexed dict into a Python list. + + Companion API sometimes returns an array as a json array, sometimes as + a json object indexed by "int". This seems to depend on whether the + there are 'gaps' in the Cmdr's data - i.e. whether the array is sparse. + In practice these arrays aren't very sparse so just convert them to + lists with any 'gaps' holding None. + """ if thing is None: return [] # data is not present @@ -132,10 +145,12 @@ def listify(thing): return retval else: - raise ValueError("expected an array or sparse array, got {!r}".format(thing)) + raise ValueError(f"expected an array or sparse array, got {thing!r}") class ServerError(Exception): + """Exception Class for CAPI ServerErrors.""" + def __init__(self, *args): # Raised when cannot contact the Companion API server self.args = args @@ -144,23 +159,34 @@ class ServerError(Exception): class ServerLagging(Exception): + """Exception Class for CAPI Server lagging. + + Raised when Companion API server is returning old data, e.g. when the + servers are too busy. + """ + def __init__(self, *args): - # Raised when Companion API server is returning old data, e.g. when the servers are too busy self.args = args if not args: self.args = (_('Error: Frontier server is lagging'),) class SKUError(Exception): + """Exception Class for CAPI SKU error. + + Raised when the Companion API server thinks that the user has not + purchased E:D i.e. doesn't have the correct 'SKU'. + """ + def __init__(self, *args): - # Raised when the Companion API server thinks that the user has not purchased E:D - # i.e. doesn't have the correct 'SKU' self.args = args if not args: self.args = (_('Error: Frontier server SKU problem'),) class CredentialsError(Exception): + """Exception Class for CAPI Credentials error.""" + def __init__(self, *args): self.args = args if not args: @@ -168,24 +194,35 @@ class CredentialsError(Exception): class CmdrError(Exception): + """Exception Class for CAPI Commander error. + + Raised when the user has multiple accounts and the username/password + setting is not for the account they're currently playing OR the user has + reset their Cmdr and the Companion API server is still returning data + for the old Cmdr. + """ + def __init__(self, *args): - # Raised when the user has multiple accounts and the username/password setting is not for - # the account they're currently playing OR the user has reset their Cmdr and the Companion API - # server is still returning data for the old Cmdr self.args = args if not args: self.args = (_('Error: Wrong Cmdr'),) class Auth(object): - def __init__(self, cmdr): + """Handles authentication with the Frontier CAPI service via oAuth2.""" + + def __init__(self, cmdr: str): self.cmdr = cmdr self.session = requests.Session() self.session.headers['User-Agent'] = USER_AGENT self.verifier = self.state = None - def refresh(self): - # Try refresh token. Returns new refresh token if successful, otherwise makes new authorization request. + def refresh(self) -> None: + """ + Attempt use of Refresh Token to get a valid Access Token. + + If the Refresh Token doesn't work, make a new authorization request. + """ logger.debug(f'Trying for "{self.cmdr}"') self.verifier = None @@ -219,7 +256,7 @@ class Auth(object): logger.error(f"Frontier CAPI Auth: Can't refresh token for \"{self.cmdr}\"") self.dump(r) - except Exception as e: + except Exception: logger.exception(f"Frontier CAPI Auth: Can't refresh token for \"{self.cmdr}\"") else: @@ -233,21 +270,17 @@ class Auth(object): self.state = self.base64_url_encode(s.to_bytes(32, byteorder='big')) # Won't work under IE: https://blogs.msdn.microsoft.com/ieinternals/2011/07/13/understanding-protocols/ logger.debug(f'Trying auth from scratch for Commander "{self.cmdr}"') + challenge = self.base64_url_encode(hashlib.sha256(self.verifier).digest()) webbrowser.open( - '{server_auth}{url_auth}?response_type=code&audience=frontier&scope=capi&client_id={client_id}&code_challenge={challenge}&code_challenge_method=S256&state={state}&redirect_uri={redirect}'.format( # noqa: E501 # I cant make this any shorter - server_auth=SERVER_AUTH, - url_auth=URL_AUTH, - client_id=CLIENT_ID, - challenge=self.base64_url_encode(hashlib.sha256(self.verifier).digest()), - state=self.state, - redirect=protocolhandler.redirect - ) + f'{SERVER_AUTH}{URL_AUTH}?response_type=code&audience=frontier&scope=capi&client_id={CLIENT_ID}&code_challenge={challenge}&code_challenge_method=S256&state={self.state}&redirect_uri={protocolhandler.redirect}' # noqa: E501 # I cant make this any shorter ) - def authorize(self, payload): + def authorize(self, payload: str) -> str: + """Handle oAuth authorization callback. + + :return: access token if successful, otherwise raises CredentialsError. + """ logger.debug('Checking oAuth authorization callback') - # Handle OAuth authorization code callback. - # Returns access token if successful, otherwise raises CredentialsError if '?' not in payload: logger.error(f'Frontier CAPI Auth: Malformed response (no "?" in payload)\n{payload}\n') raise CredentialsError('malformed payload') # Not well formed @@ -255,14 +288,14 @@ class Auth(object): data = urllib.parse.parse_qs(payload[(payload.index('?') + 1):]) if not self.state or not data.get('state') or data['state'][0] != self.state: logger.error(f'Frontier CAPI Auth: Unexpected response\n{payload}\n') - raise CredentialsError('Unexpected response from authorization {!r}'.format(payload)) # Unexpected reply + raise CredentialsError(f'Unexpected response from authorization {payload!r}') if not data.get('code'): logger.error(f'Frontier CAPI Auth: Negative response (no "code" in returned data)\n{payload}\n') error = next( (data[k] for k in ('error_description', 'error', 'message') if k in data), ('',) ) - raise CredentialsError('Error: {!r}'.format(error)[0]) + raise CredentialsError(f'Error: {error[0]!r}') try: logger.debug('Got code, posting it back...') @@ -299,10 +332,11 @@ class Auth(object): logger.error(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"") self.dump(r) error = next((data[k] for k in ('error_description', 'error', 'message') if k in data), ('',)) - raise CredentialsError('Error: {!r}'.format(error)[0]) + raise CredentialsError(f'Error: {error[0]!r}') @staticmethod - def invalidate(cmdr): + def invalidate(cmdr: str) -> None: + """Invalidate Refresh Token for specified Commander.""" logger.info(f'Frontier CAPI Auth: Invalidated token for "{cmdr}"') cmdrs = config.get('cmdrs') idx = cmdrs.index(cmdr) @@ -312,14 +346,18 @@ class Auth(object): config.set('fdev_apikeys', tokens) config.save() # Save settings now for use by command-line app - def dump(self, r): + def dump(self, r: requests.Response) -> None: + """Dump details of HTTP failure from oAuth attempt.""" logger.debug(f'Frontier CAPI Auth: {r.url} {r.status_code} {r.reason if r.reason else "None"} {r.text}') - def base64_url_encode(self, text): + def base64_url_encode(self, text: str) -> str: + """Base64 encode text for URL.""" return base64.urlsafe_b64encode(text).decode().replace('=', '') class Session(object): + """Methods for handling an oAuth2 session.""" + STATE_INIT, STATE_AUTH, STATE_OK = list(range(3)) def __init__(self): @@ -329,8 +367,12 @@ class Session(object): self.auth = None self.retrying = False # Avoid infinite loop when successful auth / unsuccessful query - def login(self, cmdr=None, is_beta=None): - # Returns True if login succeeded, False if re-authorization initiated. + def login(self, cmdr: str = None, is_beta: Union[None, bool] = None) -> bool: + """ + Attempt oAuth2 login. + + :return: True if login succeeded, False if re-authorization initiated. + """ if not CLIENT_ID: logger.error('CLIENT_ID is None') raise CredentialsError('cannot login without a valid Client ID') @@ -376,7 +418,8 @@ class Session(object): # Wait for callback # Callback from protocol handler - def auth_callback(self): + def auth_callback(self) -> None: + """Handle callback from edmc:// handler.""" logger.debug('Handling callback from edmc:// handler') if self.state != Session.STATE_AUTH: # Shouldn't be getting a callback @@ -394,14 +437,16 @@ class Session(object): self.auth = None raise # Bad thing happened - def start(self, access_token): + def start(self, access_token: str) -> None: + """Start an oAuth2 session.""" logger.debug('Starting session') self.session = requests.Session() - self.session.headers['Authorization'] = 'Bearer {}'.format(access_token) + self.session.headers['Authorization'] = f'Bearer {access_token}' self.session.headers['User-Agent'] = USER_AGENT self.state = Session.STATE_OK - def query(self, endpoint): + def query(self, endpoint: str) -> Mapping[str, Any]: + """Perform a query against the specified CAPI endpoint.""" logger.debug(f'Performing query for endpoint "{endpoint}"') if self.state == Session.STATE_INIT: if self.login(): @@ -432,7 +477,7 @@ class Session(object): # Server error. Typically 500 "Internal Server Error" if server is down logger.debug('500 status back from CAPI') self.dump(r) - raise ServerError('Received error {} from server'.format(r.status_code)) + raise ServerError(f'Received error {r.status_code} from server') try: r.raise_for_status() # Typically 403 "Forbidden" on token expiry @@ -467,36 +512,41 @@ class Session(object): return data - def profile(self): + def profile(self) -> Mapping[str, Any]: + """Perform general CAPI /profile endpoint query.""" return self.query(URL_QUERY) - def station(self): + def station(self) -> Union[Mapping[str, Any], None]: + """Perform CAPI /profile endpoint query for station data.""" data = self.query(URL_QUERY) - if data['commander'].get('docked'): - services = data['lastStarport'].get('services', {}) + if not data['commander'].get('docked'): + return None - last_starport_name = data['lastStarport']['name'] - last_starport_id = int(data['lastStarport']['id']) + services = data['lastStarport'].get('services', {}) - if services.get('commodities'): - marketdata = self.query(URL_MARKET) - if (last_starport_name != marketdata['name'] or last_starport_id != int(marketdata['id'])): - raise ServerLagging() + last_starport_name = data['lastStarport']['name'] + last_starport_id = int(data['lastStarport']['id']) - else: - data['lastStarport'].update(marketdata) + if services.get('commodities'): + marketdata = self.query(URL_MARKET) + if (last_starport_name != marketdata['name'] or last_starport_id != int(marketdata['id'])): + raise ServerLagging() - if services.get('outfitting') or services.get('shipyard'): - shipdata = self.query(URL_SHIPYARD) - if (last_starport_name != shipdata['name'] or last_starport_id != int(shipdata['id'])): - raise ServerLagging() + else: + data['lastStarport'].update(marketdata) - else: - data['lastStarport'].update(shipdata) + if services.get('outfitting') or services.get('shipyard'): + shipdata = self.query(URL_SHIPYARD) + if (last_starport_name != shipdata['name'] or last_starport_id != int(shipdata['id'])): + raise ServerLagging() + + else: + data['lastStarport'].update(shipdata) return data - def close(self): + def close(self) -> None: + """Close CAPI authorization session.""" self.state = Session.STATE_INIT if self.session: try: @@ -507,18 +557,25 @@ class Session(object): self.session = None - def invalidate(self): + def invalidate(self) -> None: + """Invalidate oAuth2 credentials.""" logger.debug('Forcing a full re-authentication') # Force a full re-authentication self.close() Auth.invalidate(self.credentials['cmdr']) - def dump(self, r): + def dump(self, r: requests.Response) -> None: + """Log, as error, status of requests.Response from CAPI request.""" logger.error(f'Frontier CAPI Auth: {r.url} {r.status_code} {r.reason and r.reason or "None"} {r.text}') -# Returns a shallow copy of the received data suitable for export to older tools -# English commodity names and anomalies fixed up -def fixup(data): + +def fixup(data: Mapping[str, Any]) -> Mapping[str, Any]: # noqa: C901, CCR001 # Can't be usefully simplified + """ + Fix up commodity names to English & miscellaneous anomalies fixes. + + :return: a shallow copy of the received data suitable for export to + older tools. + """ if not commodity_map: # Lazily populate for f in ('commodity.csv', 'rare_commodity.csv'): @@ -589,9 +646,10 @@ def fixup(data): return datacopy -# Return a subset of the received data describing the current ship -def ship(data): - def filter_ship(d): +def ship(data: Mapping[str, Any]) -> Mapping[str, Any]: + """Construct a subset of the received data describing the current ship.""" + def filter_ship(d: Mapping[str, Any]) -> Mapping[str, Any]: + """Filter provided ship data.""" filtered = {} for k, v in d.items(): if v == []: @@ -619,8 +677,8 @@ def ship(data): return filter_ship(data['ship']) -# Ship name suitable for writing to a file -def ship_file_name(ship_name, ship_type): +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_map.get(ship_type.lower(), ship_type)).strip() if name.endswith('.'): name = name[:-1] From da2a582c460ee219c40561de92250ab8a1867708 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 15 Sep 2020 14:35:37 +0100 Subject: [PATCH 03/71] companion.py: Use custom type for CAPI Data. This way if we get more stringent about it (could be a class) we don't need to edit all the references. --- companion.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/companion.py b/companion.py index b6396827..74b3d909 100644 --- a/companion.py +++ b/companion.py @@ -21,7 +21,7 @@ from email.utils import parsedate # TODO: see https://github.com/EDCD/EDMarketConnector/issues/569 from http.cookiejar import LWPCookieJar # noqa: F401 - No longer needed but retained in case plugins use it from os.path import join -from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Union +from typing import TYPE_CHECKING, Any, Dict, List, Mapping, NewType, Union import requests @@ -35,6 +35,9 @@ if TYPE_CHECKING: _ = lambda x: x # noqa: E731 # to make flake8 stop complaining that the hacked in _ method doesnt exist +# Define custom type for the dicts that hold CAPI data +CAPIData = NewType('CAPIData', Mapping[str, Any]) + holdoff = 60 # be nice timeout = 10 # requests timeout auth_timeout = 30 # timeout for initial auth @@ -445,7 +448,7 @@ class Session(object): self.session.headers['User-Agent'] = USER_AGENT self.state = Session.STATE_OK - def query(self, endpoint: str) -> Mapping[str, Any]: + def query(self, endpoint: str) -> CAPIData: """Perform a query against the specified CAPI endpoint.""" logger.debug(f'Performing query for endpoint "{endpoint}"') if self.state == Session.STATE_INIT: @@ -512,11 +515,11 @@ class Session(object): return data - def profile(self) -> Mapping[str, Any]: + def profile(self) -> CAPIData: """Perform general CAPI /profile endpoint query.""" return self.query(URL_QUERY) - def station(self) -> Union[Mapping[str, Any], None]: + def station(self) -> Union[CAPIData, None]: """Perform CAPI /profile endpoint query for station data.""" data = self.query(URL_QUERY) if not data['commander'].get('docked'): @@ -569,7 +572,7 @@ class Session(object): logger.error(f'Frontier CAPI Auth: {r.url} {r.status_code} {r.reason and r.reason or "None"} {r.text}') -def fixup(data: Mapping[str, Any]) -> Mapping[str, Any]: # noqa: C901, CCR001 # Can't be usefully simplified +def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully simplified """ Fix up commodity names to English & miscellaneous anomalies fixes. @@ -646,9 +649,9 @@ def fixup(data: Mapping[str, Any]) -> Mapping[str, Any]: # noqa: C901, CCR001 # return datacopy -def ship(data: Mapping[str, Any]) -> Mapping[str, Any]: +def ship(data: CAPIData) -> CAPIData: """Construct a subset of the received data describing the current ship.""" - def filter_ship(d: Mapping[str, Any]) -> Mapping[str, Any]: + def filter_ship(d: CAPIData) -> CAPIData: """Filter provided ship data.""" filtered = {} for k, v in d.items(): From 457535392312c2e0264220db3f9c15a162c88e78 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 15 Sep 2020 15:00:22 +0100 Subject: [PATCH 04/71] companion.py: CAPIData type & PyCharm-prompted cleanup. * CAPI Data really is just a Dict. Define a custom type so it could easily be a class in future with minimal edits. * Auth.refresh() *can* also return a str (Access Token). * Catch specific (but still quite loose) exceptions in Auth.refresh(). * Set self.server in Session.__init__. * Remove some extraneous () on conditionals. --- companion.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/companion.py b/companion.py index 74b3d909..e357bf95 100644 --- a/companion.py +++ b/companion.py @@ -13,6 +13,7 @@ import logging import numbers import os import random +import requests import time import urllib.parse import webbrowser @@ -21,22 +22,19 @@ from email.utils import parsedate # TODO: see https://github.com/EDCD/EDMarketConnector/issues/569 from http.cookiejar import LWPCookieJar # noqa: F401 - No longer needed but retained in case plugins use it from os.path import join -from typing import TYPE_CHECKING, Any, Dict, List, Mapping, NewType, Union - -import requests +from typing import TYPE_CHECKING, Dict, List, NewType, Union from config import appname, appversion, config from protocol import protocolhandler logger = logging.getLogger(appname) - if TYPE_CHECKING: _ = lambda x: x # noqa: E731 # to make flake8 stop complaining that the hacked in _ method doesnt exist # Define custom type for the dicts that hold CAPI data -CAPIData = NewType('CAPIData', Mapping[str, Any]) +CAPIData = NewType('CAPIData', Dict) holdoff = 60 # be nice timeout = 10 # requests timeout @@ -220,11 +218,13 @@ class Auth(object): self.session.headers['User-Agent'] = USER_AGENT self.verifier = self.state = None - def refresh(self) -> None: + def refresh(self) -> Union[str, None]: """ Attempt use of Refresh Token to get a valid Access Token. If the Refresh Token doesn't work, make a new authorization request. + + :return: Access Token if retrieved, else None. """ logger.debug(f'Trying for "{self.cmdr}"') @@ -239,14 +239,14 @@ class Auth(object): tokens = tokens + [''] * (len(cmdrs) - len(tokens)) if tokens[idx]: logger.debug('We have a refresh token for that idx') - try: - data = { - 'grant_type': 'refresh_token', - 'client_id': CLIENT_ID, - 'refresh_token': tokens[idx], - } + data = { + 'grant_type': 'refresh_token', + 'client_id': CLIENT_ID, + 'refresh_token': tokens[idx], + } - logger.debug('Attempting refresh with Frontier...') + logger.debug('Attempting refresh with Frontier...') + try: r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=auth_timeout) if r.status_code == requests.codes.ok: data = r.json() @@ -259,7 +259,7 @@ class Auth(object): logger.error(f"Frontier CAPI Auth: Can't refresh token for \"{self.cmdr}\"") self.dump(r) - except Exception: + except (ValueError, requests.RequestException, ): logger.exception(f"Frontier CAPI Auth: Can't refresh token for \"{self.cmdr}\"") else: @@ -300,9 +300,9 @@ class Auth(object): ) raise CredentialsError(f'Error: {error[0]!r}') + r = None try: logger.debug('Got code, posting it back...') - r = None data = { 'grant_type': 'authorization_code', 'client_id': CLIENT_ID, @@ -365,6 +365,7 @@ class Session(object): def __init__(self): self.state = Session.STATE_INIT + self.server = None self.credentials = None self.session = None self.auth = None @@ -532,7 +533,7 @@ class Session(object): if services.get('commodities'): marketdata = self.query(URL_MARKET) - if (last_starport_name != marketdata['name'] or last_starport_id != int(marketdata['id'])): + if last_starport_name != marketdata['name'] or last_starport_id != int(marketdata['id']): raise ServerLagging() else: @@ -540,7 +541,7 @@ class Session(object): if services.get('outfitting') or services.get('shipyard'): shipdata = self.query(URL_SHIPYARD) - if (last_starport_name != shipdata['name'] or last_starport_id != int(shipdata['id'])): + if last_starport_name != shipdata['name'] or last_starport_id != int(shipdata['id']): raise ServerLagging() else: @@ -646,7 +647,7 @@ def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully datacopy = data.copy() datacopy['lastStarport'] = data['lastStarport'].copy() datacopy['lastStarport']['commodities'] = commodities - return datacopy + return CAPIData(datacopy) def ship(data: CAPIData) -> CAPIData: From 55fcfbeb17937cc4fdc86334b530f18c6bb1d4d3 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 15 Sep 2020 15:33:00 +0100 Subject: [PATCH 05/71] companion.py: Further PyCharm-prompted cleanup. * Correct use of error[0] to just error. * We don't want methods not referencing self to be static in this case, so annotate them. * Take suggestion of "if v == []:" equivalent to "if not v:". * Preserve CAPIData typing throughout filter_ship(). --- companion.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/companion.py b/companion.py index e357bf95..6b742526 100644 --- a/companion.py +++ b/companion.py @@ -298,7 +298,7 @@ class Auth(object): error = next( (data[k] for k in ('error_description', 'error', 'message') if k in data), ('',) ) - raise CredentialsError(f'Error: {error[0]!r}') + raise CredentialsError(f'Error: {error!r}') r = None try: @@ -335,7 +335,7 @@ class Auth(object): logger.error(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"") self.dump(r) error = next((data[k] for k in ('error_description', 'error', 'message') if k in data), ('',)) - raise CredentialsError(f'Error: {error[0]!r}') + raise CredentialsError(f'Error: {error!r}') @staticmethod def invalidate(cmdr: str) -> None: @@ -349,11 +349,13 @@ class Auth(object): config.set('fdev_apikeys', tokens) config.save() # Save settings now for use by command-line app + # noinspection PyMethodMayBeStatic def dump(self, r: requests.Response) -> None: """Dump details of HTTP failure from oAuth attempt.""" logger.debug(f'Frontier CAPI Auth: {r.url} {r.status_code} {r.reason if r.reason else "None"} {r.text}') - def base64_url_encode(self, text: str) -> str: + # noinspection PyMethodMayBeStatic + def base64_url_encode(self, text: bytes) -> str: """Base64 encode text for URL.""" return base64.urlsafe_b64encode(text).decode().replace('=', '') @@ -568,6 +570,7 @@ class Session(object): self.close() Auth.invalidate(self.credentials['cmdr']) + # noinspection PyMethodMayBeStatic def dump(self, r: requests.Response) -> None: """Log, as error, status of requests.Response from CAPI request.""" logger.error(f'Frontier CAPI Auth: {r.url} {r.status_code} {r.reason and r.reason or "None"} {r.text}') @@ -654,9 +657,9 @@ def ship(data: CAPIData) -> CAPIData: """Construct a subset of the received data describing the current ship.""" def filter_ship(d: CAPIData) -> CAPIData: """Filter provided ship data.""" - filtered = {} + filtered: CAPIData = CAPIData({}) for k, v in d.items(): - if v == []: + if not v: pass # just skip empty fields for brevity elif k in ('alive', 'cargo', 'cockpitBreached', 'health', 'oxygenRemaining', From c311957efffaa9e4610628654377a408609cabfa Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 15 Sep 2020 15:49:28 +0100 Subject: [PATCH 06/71] companion.py: `import requests` does belong down there. --- companion.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/companion.py b/companion.py index 6b742526..b01fc7a0 100644 --- a/companion.py +++ b/companion.py @@ -13,7 +13,6 @@ import logging import numbers import os import random -import requests import time import urllib.parse import webbrowser @@ -24,6 +23,8 @@ from http.cookiejar import LWPCookieJar # noqa: F401 - No longer needed but ret from os.path import join from typing import TYPE_CHECKING, Dict, List, NewType, Union +import requests + from config import appname, appversion, config from protocol import protocolhandler From ed52528718202f859db22bf29c1f821a1c512600 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 15 Sep 2020 16:15:13 +0100 Subject: [PATCH 07/71] companion.py: Slight tweaks from running mypy NB: Using `mypy --follow-imports skip` for now to limit how much it checks each time. --- companion.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/companion.py b/companion.py index b01fc7a0..2d4ba73c 100644 --- a/companion.py +++ b/companion.py @@ -21,7 +21,7 @@ from email.utils import parsedate # TODO: see https://github.com/EDCD/EDMarketConnector/issues/569 from http.cookiejar import LWPCookieJar # noqa: F401 - No longer needed but retained in case plugins use it from os.path import join -from typing import TYPE_CHECKING, Dict, List, NewType, Union +from typing import TYPE_CHECKING, Any, Dict, List, NewType, Union import requests @@ -67,7 +67,7 @@ category_map = { 'NonMarketable': False, # Don't appear in the in-game market so don't report } -commodity_map = {} +commodity_map: Dict = {} ship_map = { 'adder': 'Adder', @@ -134,7 +134,7 @@ def listify(thing: Union[List, Dict]) -> List: return list(thing) # array is not sparse elif isinstance(thing, dict): - retval = [] + retval: List[Any] = [] for k, v in thing.items(): idx = int(k) @@ -214,10 +214,11 @@ class Auth(object): """Handles authentication with the Frontier CAPI service via oAuth2.""" def __init__(self, cmdr: str): - self.cmdr = cmdr + self.cmdr: str = cmdr self.session = requests.Session() self.session.headers['User-Agent'] = USER_AGENT - self.verifier = self.state = None + self.verifier: Union[bytes, None] = None + self.state: Union[str, None] = None def refresh(self) -> Union[str, None]: """ @@ -279,6 +280,8 @@ class Auth(object): f'{SERVER_AUTH}{URL_AUTH}?response_type=code&audience=frontier&scope=capi&client_id={CLIENT_ID}&code_challenge={challenge}&code_challenge_method=S256&state={self.state}&redirect_uri={protocolhandler.redirect}' # noqa: E501 # I cant make this any shorter ) + return None + def authorize(self, payload: str) -> str: """Handle oAuth authorization callback. @@ -304,7 +307,7 @@ class Auth(object): r = None try: logger.debug('Got code, posting it back...') - data = { + request_data = { 'grant_type': 'authorization_code', 'client_id': CLIENT_ID, 'code_verifier': self.verifier, @@ -312,7 +315,7 @@ class Auth(object): 'redirect_uri': protocolhandler.redirect, } - r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=auth_timeout) + r = self.session.post(SERVER_AUTH + URL_TOKEN, data=request_data, timeout=auth_timeout) data = r.json() if r.status_code == requests.codes.ok: logger.info(f'Frontier CAPI Auth: New token for \"{self.cmdr}\"') @@ -324,7 +327,7 @@ class Auth(object): config.set('fdev_apikeys', tokens) config.save() # Save settings now for use by command-line app - return data.get('access_token') + return str(data.get('access_token')) except Exception as e: logger.exception(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"") @@ -515,7 +518,7 @@ class Session(object): self.retrying = False if 'timestamp' not in data: logger.debug('timestamp not in data, adding from response headers') - data['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', parsedate(r.headers['Date'])) + data['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', parsedate(r.headers['Date'])) # type: ignore return data From 14d1a0ad5efb151f267b8ffebc6664f08f482a67 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 15 Sep 2020 17:30:23 +0100 Subject: [PATCH 08/71] companion.py: Re-format next(...) to be more obvious. --- companion.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/companion.py b/companion.py index 2d4ba73c..3ed92e7b 100644 --- a/companion.py +++ b/companion.py @@ -300,7 +300,8 @@ class Auth(object): if not data.get('code'): logger.error(f'Frontier CAPI Auth: Negative response (no "code" in returned data)\n{payload}\n') error = next( - (data[k] for k in ('error_description', 'error', 'message') if k in data), ('',) + (data[k] for k in ('error_description', 'error', 'message') if k in data), + '' ) raise CredentialsError(f'Error: {error!r}') @@ -338,7 +339,10 @@ class Auth(object): logger.error(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"") self.dump(r) - error = next((data[k] for k in ('error_description', 'error', 'message') if k in data), ('',)) + error = next( + (data[k] for k in ('error_description', 'error', 'message') if k in data), + '' + ) raise CredentialsError(f'Error: {error!r}') @staticmethod From c9bea9802191c17c0b40d9b94e8f60d02b2cee42 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Wed, 16 Sep 2020 09:42:05 +0100 Subject: [PATCH 09/71] Refer to Known Issues in Bug Report Template. --- .github/ISSUE_TEMPLATE/bug_report.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d0f21e70..eab87209 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,12 +1,16 @@ --- name: Bug report -about: Create a report to help us improve +about: Report an issue with the program title: '' labels: bug, unconfirmed assignees: '' --- +**Please check the +[Known Issues](https://github.com/EDCD/EDMarketConnector/issues/618) +in case this has already been reported.** + **Please complete the following information:** - Version [e.g. 4.0.6 - See 'Help > About E:D Market Connector'] - OS: [e.g. Windows 10] From 5af2511137a4ed1f035fd54b654c1f310ae06bb4 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 06:44:20 +0200 Subject: [PATCH 10/71] Updated docstrings --- plugins/inara.py | 109 +++++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 37 deletions(-) diff --git a/plugins/inara.py b/plugins/inara.py index e78287d1..b2b28ced 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -1,6 +1,4 @@ -# -# Inara sync -# +"""Inara Sync.""" import dataclasses import json @@ -28,7 +26,7 @@ from ttkHyperlinkLabel import HyperlinkLabel logger = logging.getLogger(appname) if TYPE_CHECKING: - def _(x): + def _(x: str) -> str: return x @@ -81,9 +79,8 @@ STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00 class Credentials(NamedTuple): - """ - Credentials holds the set of credentials required to identify an inara API payload to inara - """ + """Credentials holds the set of credentials required to identify an inara API payload to inara.""" + cmdr: str fid: str api_key: str @@ -93,9 +90,8 @@ EVENT_DATA = Union[Mapping[AnyStr, Any], Sequence[Mapping[AnyStr, Any]]] class Event(NamedTuple): - """ - Event represents an event for the Inara API - """ + """Event represents an event for the Inara API.""" + name: str timestamp: str data: EVENT_DATA @@ -103,12 +99,19 @@ class Event(NamedTuple): @dataclasses.dataclass class NewThis: + """ + NewThis is where the plugin stores all of its data. + + It is named NewThis as it is currently being migrated to. Once migration is complete it will be renamed to This. + """ + events: Dict[Credentials, Deque[Event]] = dataclasses.field(default_factory=lambda: defaultdict(deque)) event_lock: Lock = dataclasses.field(default_factory=Lock) # protects events, for use when rewriting events - def filter_events(self, key: Credentials, predicate: Callable[[Event], bool]): + def filter_events(self, key: Credentials, predicate: Callable[[Event], bool]) -> None: """ filter_events is the equivalent of running filter() on any event list in the events dict. + it will automatically handle locking, and replacing the event list with the filtered version. :param key: the key to filter @@ -124,7 +127,8 @@ new_this = NewThis() TARGET_URL = 'https://inara.cz/inapi/v1/' -def system_url(system_name: str): +def system_url(system_name: str) -> str: + """Get a URL for the current system.""" if this.system_address: return requests.utils.requote_uri(f'https://inara.cz/galaxy-starsystem/?search={this.system_address}') @@ -133,7 +137,17 @@ def system_url(system_name: str): return '' -def station_url(system_name, station_name): + +def station_url(system_name: str, station_name: str) -> str: + """ + Get a URL for the current station. + + If there is no station, the system URL is returned. + + :param system_name: The name of the current system + :param station_name: The name of the current station, if any + :return: A URL to inara for the given system and station + """ if system_name and station_name: return requests.utils.requote_uri(f'https://inara.cz/galaxy-station/?search={system_name}%20[{station_name}]') @@ -146,7 +160,9 @@ def station_url(system_name, station_name): return '' -def plugin_start3(plugin_dir): + +def plugin_start3(plugin_dir: str) -> str: + """Plugin start Hook.""" this.thread = Thread(target=new_worker, name='Inara worker') this.thread.daemon = True this.thread.start() @@ -154,14 +170,16 @@ def plugin_start3(plugin_dir): return 'Inara' -def plugin_app(parent: tk.Tk): +def plugin_app(parent: tk.Tk) -> None: + """Plugin UI setup Hook.""" this.system_link = parent.children['system'] # system label in main window this.station_link = parent.children['station'] # station label in main window this.system_link.bind_all('<>', update_location) this.system_link.bind_all('<>', update_ship) -def plugin_stop(): +def plugin_stop() -> None: + """Plugin shutdown hook.""" # Signal thread to close and wait for it this.queue.put(None) # this.thread.join() @@ -170,7 +188,8 @@ def plugin_stop(): this.timer_run = False -def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool): +def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: + """Plugin Preferences UI hook.""" PADX = 10 BUTTONX = 12 # indent Checkbuttons and Radiobuttons PADY = 2 # close spacing @@ -212,7 +231,8 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool): return frame -def prefs_cmdr_changed(cmdr: str, is_beta: bool): +def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: + """Plugin commander change hook.""" this.log_button['state'] = tk.NORMAL if cmdr and not is_beta else tk.DISABLED this.apikey['state'] = tk.NORMAL this.apikey.delete(0, tk.END) @@ -231,6 +251,7 @@ def prefs_cmdr_changed(cmdr: str, is_beta: bool): def prefsvarchanged(): + """Preferences window change hook.""" state = tk.DISABLED if this.log.get(): state = this.log_button['state'] @@ -240,7 +261,8 @@ def prefsvarchanged(): this.apikey['state'] = state -def prefs_changed(cmdr: str, is_beta: bool): +def prefs_changed(cmdr: str, is_beta: bool) -> None: + """Preferences window closed hook.""" changed = config.getint('inara_out') != this.log.get() config.set('inara_out', this.log.get()) @@ -264,14 +286,14 @@ def prefs_changed(cmdr: str, is_beta: bool): if this.log.get() and changed: this.newuser = True # Send basic info at next Journal event - new_add_event('getCommanderProfile', time.strftime( - '%Y-%m-%dT%H:%M:%SZ', time.gmtime()), {'searchName': cmdr}) - # call() + new_add_event( + 'getCommanderProfile', time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), {'searchName': cmdr} + ) def credentials(cmdr: str) -> Optional[str]: """ - credentials fetches the credentials for the given commander + Get the credentials for the current commander. :param cmdr: Commander name to search for credentials :return: Credentials for the given commander or None @@ -287,11 +309,10 @@ def credentials(cmdr: str) -> Optional[str]: return None -def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Dict[str, Any], state: Dict[str, Any]): - # Send any unsent events when switching accounts - # if cmdr and cmdr != this.cmdr: - # call(force=True) - +def journal_entry( + cmdr: str, is_beta: bool, system: str, station: str, entry: Dict[str, Any], state: Dict[str, Any] +) -> None: + """Journal entry hook.""" event_name = entry['event'] this.cmdr = cmdr this.FID = state['FID'] @@ -1012,6 +1033,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di def cmdr_data(data, is_beta): + """CAPI event hook.""" this.cmdr = data['commander']['name'] # Always store initially, even if we're not the *current* system provider. @@ -1049,7 +1071,8 @@ def cmdr_data(data, is_beta): if not (CREDIT_RATIO > this.lastcredits / data['commander']['credits'] > 1/CREDIT_RATIO): new_this.filter_events( Credentials(this.cmdr, this.FID, str(credentials(this.cmdr))), - lambda e: e.name != 'setCommanderCredits') + lambda e: e.name != 'setCommanderCredits' + ) # this.events = [x for x in this.events if x['eventName'] != 'setCommanderCredits'] # Remove any unsent new_add_event( @@ -1065,6 +1088,12 @@ def cmdr_data(data, is_beta): def make_loadout(state: Dict[str, Any]) -> OrderedDictT[str, Any]: + """ + Construct an inara loadout from an event. + + :param state: The event / state to construct the event from + :return: The constructed loadout + """ modules = [] for m in state['Modules'].values(): module: OrderedDictT[str, Any] = OrderedDict([ @@ -1132,8 +1161,9 @@ def new_add_event( fid: Optional[str] = None ): """ - add a journal event to the queue, to be sent to inara at the next opportunity. If provided, use the given cmdr - name over the current one + Add a journal event to the queue, to be sent to inara at the next opportunity. + + If provided, use the given cmdr name over the current one :param name: name of the event :param timestamp: timestamp of the event @@ -1158,6 +1188,11 @@ def new_add_event( def new_worker(): + """ + Queue worker. + + Will only ever send one message per WORKER_WAIT_TIME, regardless of status. + """ while True: events = get_events() for creds, event_list in events.items(): @@ -1184,7 +1219,7 @@ def new_worker(): def get_events(clear=True) -> Dict[Credentials, List[Event]]: """ - get_events fetches all events from the current queue and returns a frozen version of them + Fetch a frozen copy of all events from the current queue. :param clear: whether or not to clear the queues as we go, defaults to True :return: the frozen event list @@ -1201,7 +1236,7 @@ def get_events(clear=True) -> Dict[Credentials, List[Event]]: def try_send_data(url: str, data: Mapping[str, Any]): """ - attempt repeatedly to send the payload forward + Attempt repeatedly to send the payload forward. :param url: target URL for the payload :param data: the payload @@ -1219,7 +1254,7 @@ def try_send_data(url: str, data: Mapping[str, Any]): def send_data(url: str, data: Mapping[str, Any]) -> bool: """ - write a set of events to the inara API + Write a set of events to the inara API. :param url: the target URL to post to :param data: the data to POST @@ -1268,8 +1303,7 @@ def send_data(url: str, data: Mapping[str, Any]) -> bool: def update_location(event=None): """ - Call inara_notify_location in this and other interested plugins with Inara's response when changing system - or station + Update other plugins with our response to system and station changes. :param event: Unused and ignored, defaults to None """ @@ -1279,12 +1313,13 @@ def update_location(event=None): def inara_notify_location(eventData): + """Unused.""" pass def update_ship(event=None): """ - Call inara_notify_ship() in interested plugins with Inara's response when changing ship + Update other plugins with our response to changing. :param event: Unused and ignored, defaults to None """ From 6fc2ab063e5ff768dfac00c496579d3451c47d04 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 07:28:30 +0200 Subject: [PATCH 11/71] finished type annotation --- plugins/inara.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/inara.py b/plugins/inara.py index b2b28ced..e1b1efd8 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -11,9 +11,11 @@ from collections import OrderedDict, defaultdict, deque from operator import itemgetter from queue import Queue from threading import Lock, Thread -from typing import TYPE_CHECKING, Any, AnyStr, Callable, Deque, Dict, List, Mapping, NamedTuple, Optional +from typing import ( + TYPE_CHECKING, Any, AnyStr, Callable, Deque, Dict, List, Mapping, MutableMapping, NamedTuple, Optional +) from typing import OrderedDict as OrderedDictT -from typing import Sequence, Union +from typing import Sequence, Union, cast import requests @@ -313,7 +315,7 @@ def journal_entry( cmdr: str, is_beta: bool, system: str, station: str, entry: Dict[str, Any], state: Dict[str, Any] ) -> None: """Journal entry hook.""" - event_name = entry['event'] + event_name: str = entry['event'] this.cmdr = cmdr this.FID = state['FID'] this.multicrew = bool(state['Role']) @@ -358,7 +360,7 @@ def journal_entry( # We need pop == 0 to set the value so as to clear 'x' in systems with # no stations. - pop = entry.get('Population') + pop: Optional[int] = entry.get('Population') if pop is not None: this.system_population = pop @@ -373,10 +375,7 @@ def journal_entry( current_creds = Credentials(this.cmdr, this.FID, str(credentials(this.cmdr))) try: # Dump starting state to Inara - if (this.newuser or - event_name == 'StartUp' or - (this.newsession and event_name == 'Cargo')): - + if (this.newuser or event_name == 'StartUp' or (this.newsession and event_name == 'Cargo')): this.newuser = False this.newsession = False @@ -400,7 +399,7 @@ def journal_entry( ) if state['Engineers']: # Not populated < 3.3 - to_send = [] + to_send: List[Mapping[str, Any]] = [] for k, v in state['Engineers'].items(): e = {'engineerName': k} if isinstance(v, tuple): @@ -461,6 +460,7 @@ def journal_entry( ) elif event_name == 'EngineerProgress' and 'Engineer' in entry: + # TODO: due to this var name being used above, the types are weird to_send = {'engineerName': entry['Engineer']} if 'Rank' in entry: to_send['rankValue'] = entry['Rank'] @@ -498,7 +498,7 @@ def journal_entry( # Ship change if event_name == 'Loadout' and this.shipswap: - cur_ship = { + cur_ship: Dict[str, Any] = { 'shipType': state['ShipType'], 'shipGameID': state['ShipID'], 'shipName': state['ShipName'], # Can be None @@ -616,7 +616,7 @@ def journal_entry( new_add_event('setCommanderInventoryCargo', entry['timestamp'], cargo) this.cargo = cargo - materials = [] + materials: List[Mapping[str, Any]] = [] for category in ('Raw', 'Manufactured', 'Encoded'): materials.extend( [OrderedDict([('itemName', k), ('itemCount', state[category][k])]) for k in sorted(state[category])] @@ -950,7 +950,7 @@ def journal_entry( # )) for goal in entry['CurrentGoals']: - data = OrderedDict([ + data: MutableMapping[str, Any] = OrderedDict([ ('communitygoalGameID', goal['CGID']), ('communitygoalName', goal['Title']), ('starsystemName', goal['SystemName']), @@ -973,7 +973,7 @@ def journal_entry( new_add_event('setCommunityGoal', entry['timestamp'], data) - data = OrderedDict([ + data: MutableMapping[str, Any] = OrderedDict([ ('communitygoalGameID', goal['CGID']), ('contribution', goal['PlayerContribution']), ('percentileBand', goal['PlayerPercentileBand']), @@ -1019,7 +1019,7 @@ def journal_entry( this.system_link.update_idletasks() if config.get('station_provider') == 'Inara': - to_set = this.station + to_set: str = cast(str, this.station) if not to_set: if this.system_population is not None and this.system_population > 0: to_set = STATION_UNDOCKED @@ -1217,7 +1217,7 @@ def new_worker(): time.sleep(WORKER_WAIT_TIME) -def get_events(clear=True) -> Dict[Credentials, List[Event]]: +def get_events(clear: bool = True) -> Dict[Credentials, List[Event]]: """ Fetch a frozen copy of all events from the current queue. @@ -1234,7 +1234,7 @@ def get_events(clear=True) -> Dict[Credentials, List[Event]]: return out -def try_send_data(url: str, data: Mapping[str, Any]): +def try_send_data(url: str, data: Mapping[str, Any]) -> None: """ Attempt repeatedly to send the payload forward. @@ -1301,7 +1301,7 @@ def send_data(url: str, data: Mapping[str, Any]) -> bool: return True # regardless of errors above, we DID manage to send it, therefore inform our caller as such -def update_location(event=None): +def update_location(event: Dict[str, Any] = None) -> None: """ Update other plugins with our response to system and station changes. @@ -1312,12 +1312,12 @@ def update_location(event=None): plug.invoke(plugin, None, 'inara_notify_location', this.lastlocation) -def inara_notify_location(eventData): +def inara_notify_location(eventData: Dict[str, Any]) -> None: """Unused.""" pass -def update_ship(event=None): +def update_ship(event: Dict[str, Any] = None) -> None: """ Update other plugins with our response to changing. From 81f71c88ffb013cc3fc0f3c322e88f041dddbdb8 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 11:15:15 +0100 Subject: [PATCH 12/71] Catch where station modules from CAPI is an empty list. Out of paranoia we're also checking if it's a non-empty list and logging that as an error if encountered. It should be a dictionary! --- plugins/eddn.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/plugins/eddn.py b/plugins/eddn.py index e03cb3cb..77c1a767 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -271,8 +271,13 @@ Msg:\n{msg}''') :param is_beta: whether or not we're currently in beta mode """ modules: Dict[str, Any] = data['lastStarport'].get('modules') - if modules is None: - logger.debug('modules was None') + if modules is None or isinstance(modules, list): + if modules is None: + logger.debug('modules was None') + elif modules == []: + logger.debug('modules is empty list') + else: + logger.error(f'modules is non-empty list: {modules!r}') modules = {} ships: Dict[str, Any] = data['lastStarport'].get('ships') @@ -322,8 +327,13 @@ Msg:\n{msg}''') :param is_beta: whether or not we are in beta mode """ modules: Dict[str, Any] = data['lastStarport'].get('modules') - if modules is None: - logger.debug('modules was None') + if modules is None or isinstance(modules, list): + if modules is None: + logger.debug('modules was None') + elif modules == []: + logger.debug('modules is empty list') + else: + logger.error(f'modules is non-empty list: {modules!r}') modules = {} ships: Dict[str, Any] = data['lastStarport'].get('ships') From 22a8da5bf7ebcb82e7d7609b4fa24e680f84932a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 11:38:20 +0100 Subject: [PATCH 13/71] Add ships sanity checking, and it's all in a helper function now. * Moved these modules and ships checks to safe_modules_and_ships(). * Applied some paranoia checks to ships as well. --- plugins/eddn.py | 60 ++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/plugins/eddn.py b/plugins/eddn.py index 77c1a767..acfb39c8 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -11,9 +11,9 @@ from collections import OrderedDict from os import SEEK_SET from os.path import join from platform import system -from typing import TYPE_CHECKING, Any, AnyStr, Dict, Iterator, List, Mapping, MutableMapping, Optional +from typing import TYPE_CHECKING, Any, AnyStr, Dict, Iterator, List, Mapping, MutableMapping, Optional, Tuple from typing import OrderedDict as OrderedDictT -from typing import Sequence, TextIO, Tuple +from typing import Sequence, TextIO import requests @@ -262,6 +262,32 @@ Msg:\n{msg}''') this.commodities = commodities + def safe_modules_and_ships(self, data: Mapping[str, Any]) -> Tuple[Dict, Dict]: + modules: Dict[str, Any] = data['lastStarport'].get('modules') + if modules is None or not isinstance(modules, dict): + if modules is None: + logger.debug('modules was None. FC or Damaged Station?') + elif isinstance(modules, list): + if len(modules) == 0: + logger.debug('modules is empty list. Damaged Station?') + else: + logger.error(f'modules is non-empty list: {modules!r}') + else: + logger.error(f'modules was not None, a list, or a dict! type = {type(modules)}') + # Set a safe value + modules = {} + + ships: Dict[str, Any] = data['lastStarport'].get('ships') + if ships is None or not isinstance(ships, dict): + if ships is None: + logger.debug('ships was None') + else: + logger.error(f'ships was neither None nor a Dict! Type = {type(ships)}') + # Set a safe value + ships = {'shipyard_list': {}, 'unavailable_list': []} + + return (modules, ships) + def export_outfitting(self, data: Mapping[str, Any], is_beta: bool) -> None: """ export_outfitting updates EDDN with the current (lastStarport) station's outfitting options, if any. @@ -270,20 +296,7 @@ Msg:\n{msg}''') :param data: dict containing the outfitting data :param is_beta: whether or not we're currently in beta mode """ - modules: Dict[str, Any] = data['lastStarport'].get('modules') - if modules is None or isinstance(modules, list): - if modules is None: - logger.debug('modules was None') - elif modules == []: - logger.debug('modules is empty list') - else: - logger.error(f'modules is non-empty list: {modules!r}') - modules = {} - - ships: Dict[str, Any] = data['lastStarport'].get('ships') - if ships is None: - logger.debug('ships was None') - ships = {'shipyard_list': {}, 'unavailable_list': []} + (modules, ships) = self.safe_modules_and_ships(data) # Horizons flag - will hit at least Int_PlanetApproachSuite other than at engineer bases ("Colony"), # prison or rescue Megaships, or under Pirate Attack etc @@ -326,20 +339,7 @@ Msg:\n{msg}''') :param data: dict containing the shipyard data :param is_beta: whether or not we are in beta mode """ - modules: Dict[str, Any] = data['lastStarport'].get('modules') - if modules is None or isinstance(modules, list): - if modules is None: - logger.debug('modules was None') - elif modules == []: - logger.debug('modules is empty list') - else: - logger.error(f'modules is non-empty list: {modules!r}') - modules = {} - - ships: Dict[str, Any] = data['lastStarport'].get('ships') - if ships is None: - logger.debug('ships was None') - ships = {'shipyard_list': {}, 'unavailable_list': []} + (modules, ships) = self.safe_modules_and_ships(data) horizons: bool = is_horizons( data['lastStarport'].get('economies', {}), From ed8403f30372d5ac0034c52d95cfce85cd699545 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 12:17:08 +0100 Subject: [PATCH 14/71] Plugin logging needs to be DEBUG, to let channels decide later. This should have been changed when the rotating file logging was implemented. --- EDMCLogging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EDMCLogging.py b/EDMCLogging.py index 4208dc28..62aab91a 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -41,7 +41,7 @@ from config import appname, config # # 14. Call from *package* -_default_loglevel = logging.INFO +_default_loglevel = logging.DEBUG class Logger: @@ -326,6 +326,7 @@ class EDMCContextFilter(logging.Filter): return module_name + # Singleton loglevel = config.get('loglevel') if not loglevel: From be541571d9d05105f1c68a04bbc14782370c4602 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 12:48:21 +0100 Subject: [PATCH 15/71] companion.py: Return CAPI data if not docked Else at least one caller to .station() blows up when it can't find the commander data. The call has to be to .station() so as to populate the market/shipyard added dictionaries. --- companion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/companion.py b/companion.py index 3ed92e7b..9176fc2e 100644 --- a/companion.py +++ b/companion.py @@ -534,7 +534,7 @@ class Session(object): """Perform CAPI /profile endpoint query for station data.""" data = self.query(URL_QUERY) if not data['commander'].get('docked'): - return None + return data services = data['lastStarport'].get('services', {}) From 030ae05889f440e366ed1eeca8dc749b2b74af64 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 12:58:16 +0100 Subject: [PATCH 16/71] Adjust return type of companion.session.station() Now we're correctly always returning `data` it can't be None. --- companion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/companion.py b/companion.py index 9176fc2e..3be504f4 100644 --- a/companion.py +++ b/companion.py @@ -530,7 +530,7 @@ class Session(object): """Perform general CAPI /profile endpoint query.""" return self.query(URL_QUERY) - def station(self) -> Union[CAPIData, None]: + def station(self) -> CAPIData: """Perform CAPI /profile endpoint query for station data.""" data = self.query(URL_QUERY) if not data['commander'].get('docked'): From f485ffa737e12af5b03106fad242e405309a94d9 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 12:59:09 +0100 Subject: [PATCH 17/71] Correctly return and receive bare Tuple, not nested. --- plugins/eddn.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/eddn.py b/plugins/eddn.py index acfb39c8..2001d321 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -286,7 +286,7 @@ Msg:\n{msg}''') # Set a safe value ships = {'shipyard_list': {}, 'unavailable_list': []} - return (modules, ships) + return modules, ships def export_outfitting(self, data: Mapping[str, Any], is_beta: bool) -> None: """ @@ -296,7 +296,7 @@ Msg:\n{msg}''') :param data: dict containing the outfitting data :param is_beta: whether or not we're currently in beta mode """ - (modules, ships) = self.safe_modules_and_ships(data) + modules, ships = self.safe_modules_and_ships(data) # Horizons flag - will hit at least Int_PlanetApproachSuite other than at engineer bases ("Colony"), # prison or rescue Megaships, or under Pirate Attack etc @@ -339,7 +339,7 @@ Msg:\n{msg}''') :param data: dict containing the shipyard data :param is_beta: whether or not we are in beta mode """ - (modules, ships) = self.safe_modules_and_ships(data) + modules, ships = self.safe_modules_and_ships(data) horizons: bool = is_horizons( data['lastStarport'].get('economies', {}), From a19fd3cbf6d7956ea730e6953934544e0ff792aa Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 13:07:44 +0100 Subject: [PATCH 18/71] PLUGINS.md: Emphasise the correct way to get a logger. A_D had accidentally used 'ClickCounter' for this when their folder name was 'click_counter'. This lead to using a very bare `logger` which still used our defined format leading to things blowing up when there was no adapter to handle `qualname` format string. --- PLUGINS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/PLUGINS.md b/PLUGINS.md index c391a3ab..3eba7d06 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -82,6 +82,8 @@ plugin_name = os.path.basename(os.path.dirname(__file__)) # A Logger is used per 'found' plugin to make it easy to include the plugin's # folder name in the logging output format. +# NB: plugin_name here *must* be the plugin's folder name as per the preceding +# code, else the logger won't be properly set up. logger = logging.getLogger(f'{appname}.{plugin_name}') # If the Logger has handlers then it was already set up by the core code, else @@ -98,6 +100,13 @@ if not logger.hasHandlers(): logger.addHandler(logger_channel) ``` +Note the admonishment about `plugin_name` being the folder name of your plugin. +It can't be anything else (such as a different string returned from +`plugin_start3()`) because the code in plug.py that sets up the logger uses +exactly the folder name. Our custom `qualname` and `class` formatters won't +work with a 'bare' logger, and will cause your code to throw exceptions if +you're not using our supplied logger. + If running with 4.1.0-beta1 or later of EDMC the logging setup happens in the core code and will include the extra logfile destinations. If your plugin is run under a pre-4.1.0 version of EDMC then the above will set up From 3f001a383f5b8235d14c46bf5c56fa5046e1da41 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 15:15:02 +0100 Subject: [PATCH 19/71] plugins/edsm: Add extra logging for CarrierJump events. We've had reports about EDSM not updating for these, so add logging to see if it's a bug our end. --- plugins/edsm.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 806536cb..6753921d 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -217,6 +217,15 @@ def credentials(cmdr): def journal_entry(cmdr, is_beta, system, station, entry, state): + if entry['event'] == 'CarrierJump': + logger.debug(f'''CarrierJump +Commander: {cmdr} +System: {system} +Station: {station} +state: {state!r} +entry: {entry!r}''' + ) + # Always update our system address even if we're not currently the provider for system or station, but dont update # on events that contain "future" data, such as FSDTarget if entry['event'] in ('Location', 'Docked', 'CarrierJump', 'FSDJump'): @@ -291,6 +300,10 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): materials.update(transient) this.queue.put((cmdr, materials)) + if entry['event'] == 'CarrierJump': + logger.debug(f'''CarrierJump +Queueing: {entry!r}''' + ) this.queue.put((cmdr, entry)) @@ -346,7 +359,12 @@ def worker(): retrying = 0 while retrying < 3: try: + if item and entry['event'] == 'CarrierJump': + logger.debug('CarrierJump') + if item and entry['event'] not in this.discardedEvents: + if entry['event'] == 'CarrierJump': + logger.debug(f'CarrierJump event not in discarded list') pending.append(entry) # Get list of events to discard @@ -359,6 +377,9 @@ def worker(): pending = [x for x in pending if x['event'] not in this.discardedEvents] # Filter out unwanted events if should_send(pending): + if any([p for p in pending if p['event'] == 'CarrierJump']): + logger.debug('CarrierJump in pending and it passed should_send()') + (username, apikey) = credentials(cmdr) data = { 'commanderName': username.encode('utf-8'), @@ -367,6 +388,13 @@ def worker(): 'fromSoftwareVersion': appversion, 'message': json.dumps(pending, ensure_ascii=False).encode('utf-8'), } + + if any([p for p in pending if p['event'] == 'CarrierJump']): + data_elided = data.copy() + data_elided['apiKey'] = '' + logger.debug(f'''CarrierJump: Attempting API call +data: {data_elided!r}''' + ) r = this.session.post('https://www.edsm.net/api-journal-v1', data=data, timeout=_TIMEOUT) r.raise_for_status() reply = r.json() @@ -448,4 +476,3 @@ def edsm_notify_system(reply): this.system_link['image'] = this._IMG_NEW else: this.system_link['image'] = this._IMG_KNOWN - From 0efe38540f8af7576fa058a457c480da329322f5 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 15:33:18 +0100 Subject: [PATCH 20/71] Extend logging to FSDJump as well for ease of testing. FSDJump happens, relatively, seldom enough to not be too spammy. --- plugins/edsm.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 6753921d..44a75131 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -217,8 +217,8 @@ def credentials(cmdr): def journal_entry(cmdr, is_beta, system, station, entry, state): - if entry['event'] == 'CarrierJump': - logger.debug(f'''CarrierJump + if entry['event'] in ('CarrierJump', 'FSDJump'): + logger.debug(f'''{entry["event"]} Commander: {cmdr} System: {system} Station: {station} @@ -300,8 +300,8 @@ entry: {entry!r}''' materials.update(transient) this.queue.put((cmdr, materials)) - if entry['event'] == 'CarrierJump': - logger.debug(f'''CarrierJump + if entry['event'] in ('CarrierJump', 'FSDJump'): + logger.debug(f'''{entry["event"]} Queueing: {entry!r}''' ) this.queue.put((cmdr, entry)) @@ -359,12 +359,12 @@ def worker(): retrying = 0 while retrying < 3: try: - if item and entry['event'] == 'CarrierJump': - logger.debug('CarrierJump') + if item and entry['event'] in ('CarrierJump', 'FSDJump'): + logger.debug(f'{entry["event"]}') if item and entry['event'] not in this.discardedEvents: - if entry['event'] == 'CarrierJump': - logger.debug(f'CarrierJump event not in discarded list') + if entry['event'] in ('CarrierJump', 'FSDJump'): + logger.debug(f'{entry["event"]} event not in discarded list') pending.append(entry) # Get list of events to discard @@ -377,8 +377,8 @@ def worker(): pending = [x for x in pending if x['event'] not in this.discardedEvents] # Filter out unwanted events if should_send(pending): - if any([p for p in pending if p['event'] == 'CarrierJump']): - logger.debug('CarrierJump in pending and it passed should_send()') + if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump')]): + logger.debug('CarrierJump (or FSDJump) in pending and it passed should_send()') (username, apikey) = credentials(cmdr) data = { @@ -389,10 +389,10 @@ def worker(): 'message': json.dumps(pending, ensure_ascii=False).encode('utf-8'), } - if any([p for p in pending if p['event'] == 'CarrierJump']): + if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump')]): data_elided = data.copy() data_elided['apiKey'] = '' - logger.debug(f'''CarrierJump: Attempting API call + logger.debug(f'''CarrierJump (or FSDJump): Attempting API call data: {data_elided!r}''' ) r = this.session.post('https://www.edsm.net/api-journal-v1', data=data, timeout=_TIMEOUT) From 3516f1cf6d9bb3244748f50a54b14e07a028c686 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 15:42:56 +0100 Subject: [PATCH 21/71] Translations: Updated with latest from OneSkyApp --- L10n/cs.strings | 6 ------ L10n/de.strings | 6 ------ L10n/en.template | 12 ++++++------ L10n/fi.strings | 8 ++++++++ L10n/it.strings | 3 --- L10n/ja.strings | 6 ------ L10n/pt-BR.strings | 6 ------ L10n/pt-PT.strings | 8 ++++++++ L10n/ru.strings | 43 ++++++++++++++++++++++------------------- L10n/sr-Latn-BA.strings | 6 ------ L10n/sr-Latn.strings | 8 ++++++++ 11 files changed, 53 insertions(+), 59 deletions(-) diff --git a/L10n/cs.strings b/L10n/cs.strings index 36e26ab8..fad071e0 100644 --- a/L10n/cs.strings +++ b/L10n/cs.strings @@ -1,9 +1,6 @@ /* Label for 'UI Scaling' option [prefs.py] */ "UI Scale Percentage" = "Procenta UI škálování"; - -/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */ -"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 je výchozí hodnota{CR}Je potřeba restartovat{CR}aby se změna projevila!"; /* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ "100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 je výchozí hodnota{CR}je potřeba restartovat,{CR}aby se změna projevila!"; @@ -535,8 +532,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} vyžaduje oprávnění pro použití klávesových zkratek"; - -/* Label for 'UI Scaling' option [prefs.py] */ -"UI Scaling" = "UI škálování"; /* Label for user configured level of logging [prefs.py] */ "Log Level" = "Míra logování"; diff --git a/L10n/de.strings b/L10n/de.strings index e70acc1a..73239607 100644 --- a/L10n/de.strings +++ b/L10n/de.strings @@ -1,9 +1,6 @@ /* Label for 'UI Scaling' option [prefs.py] */ "UI Scale Percentage" = "UI-Skalierung in Prozent"; - -/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */ -"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 bedeutet Standardwert{CR}Neustart erforderlich, um{CR}Änderungen zu übernehmen!"; /* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ "100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 bedeutet Standardwert{CR}Neustart erforderlich, um{CR}Änderungen zu übernehmen!"; @@ -535,8 +532,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} benötigt für Tastenkürzel Systemrechte (Bedienungshilfen)"; - -/* Label for 'UI Scaling' option [prefs.py] */ -"UI Scaling" = "UI-Skalierung"; /* Label for user configured level of logging [prefs.py] */ "Log Level" = "Log-Level"; diff --git a/L10n/en.template b/L10n/en.template index a8ca6dc6..cf14aa37 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -1,3 +1,9 @@ +/* Label for 'UI Scaling' option [prefs.py] */ +"UI Scale Percentage" = "UI Scale Percentage"; + +/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ +"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!"; + /* Language name */ "!Language" = "English"; @@ -526,11 +532,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} needs permission to use shortcuts"; -/* Label for 'UI Scaling' option [prefs.py] */ -"UI Scale Percentage" = "UI Scale Percentage"; - -/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ -"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!"; - /* Label for user configured level of logging [prefs.py] */ "Log Level" = "Log Level"; diff --git a/L10n/fi.strings b/L10n/fi.strings index fd0ff40a..e3e5eacb 100644 --- a/L10n/fi.strings +++ b/L10n/fi.strings @@ -1,3 +1,9 @@ +/* Label for 'UI Scaling' option [prefs.py] */ +"UI Scale Percentage" = "Käyttöliittymän skaalausprosentti"; + +/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ +"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 tarkoittaa oletusta{CR}Muutosten käyttöönotto{CR}edellyttää uudelleenkäynnistystä!"; + /* Language name */ "!Language" = "Suomi"; @@ -526,3 +532,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} tarvitsee lupasi pikakuvakkeiden käyttämiseen"; +/* Label for user configured level of logging [prefs.py] */ +"Log Level" = "Lokin syvyys"; diff --git a/L10n/it.strings b/L10n/it.strings index a6659043..0706161f 100644 --- a/L10n/it.strings +++ b/L10n/it.strings @@ -526,8 +526,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} ha bisogno dei permessi per usare le scorciatoie"; - -/* Label for 'UI Scaling' option [prefs.py] */ -"UI Scaling" = "Adattamento UI"; /* Label for user configured level of logging [prefs.py] */ "Log Level" = "Livello di Log"; diff --git a/L10n/ja.strings b/L10n/ja.strings index 61d89a98..64bca474 100644 --- a/L10n/ja.strings +++ b/L10n/ja.strings @@ -1,9 +1,6 @@ /* Label for 'UI Scaling' option [prefs.py] */ "UI Scale Percentage" = "UIスケール比率"; - -/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */ -"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0はデフォルトを意味します{CR}変更した値を反映するには{CR}再起動が必要です!"; /* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ "100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100はデフォルトを意味します{CR}変更した値を反映するには{CR}再起動が必要です!"; @@ -535,8 +532,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} がショートカットを利用するには許可が必要です"; - -/* Label for 'UI Scaling' option [prefs.py] */ -"UI Scaling" = "UIスケーリング"; /* Label for user configured level of logging [prefs.py] */ "Log Level" = "ログレベル"; diff --git a/L10n/pt-BR.strings b/L10n/pt-BR.strings index eaeb23b1..7e6b1e5f 100644 --- a/L10n/pt-BR.strings +++ b/L10n/pt-BR.strings @@ -1,9 +1,6 @@ /* Label for 'UI Scaling' option [prefs.py] */ "UI Scale Percentage" = "Porcentagem de escala da IU"; - -/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */ -"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 significa Padrão{CR}Necessário Reiniciar para as{CR}alterações entrarem em vigor!"; /* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ "100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 significa Padrão{CR}Necessário Reiniciar para as{CR}alterações entrarem em vigor!"; @@ -535,8 +532,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} precisa de permissão para usar atalhos"; - -/* Label for 'UI Scaling' option [prefs.py] */ -"UI Scaling" = "Escala da UI"; /* Label for user configured level of logging [prefs.py] */ "Log Level" = "Nível de Log"; diff --git a/L10n/pt-PT.strings b/L10n/pt-PT.strings index 9cd3d0b7..79542cf1 100644 --- a/L10n/pt-PT.strings +++ b/L10n/pt-PT.strings @@ -1,3 +1,9 @@ +/* Label for 'UI Scaling' option [prefs.py] */ +"UI Scale Percentage" = "Percentagem da escala de UI"; + +/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ +"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 significa Predefinido{CR}Requer reinício para{CR}que as mudanças façam efeito!"; + /* Language name */ "!Language" = "Português (Portugal)"; @@ -526,3 +532,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "A {APP} precisa de autorização para usar atalhos"; +/* Label for user configured level of logging [prefs.py] */ +"Log Level" = "Nível de Log"; diff --git a/L10n/ru.strings b/L10n/ru.strings index b31c20ac..64a8c6dc 100644 --- a/L10n/ru.strings +++ b/L10n/ru.strings @@ -1,3 +1,9 @@ +/* Label for 'UI Scaling' option [prefs.py] */ +"UI Scale Percentage" = "Процент масштабирования интерфейса"; + +/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ +"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 - значение по умолчанию{CR}требуется перезапуск для вступления в силу изменений{CR}!"; + /* Language name */ "!Language" = "Русский"; @@ -56,7 +62,7 @@ "Chief Petty Officer" = "Главный старшина"; /* Main window. [EDMarketConnector.py] */ -"Cmdr" = "Командир"; +"Cmdr" = "КМДР"; /* Ranking. [stats.py] */ "Combat" = "Боевой"; @@ -86,7 +92,7 @@ "Dangerous" = "Опасный"; /* Appearance theme setting. [prefs.py] */ -"Dark" = "Темная"; +"Dark" = "Тёмная"; /* Combat rank. [stats.py] */ "Deadly" = "Смертоносный"; @@ -101,7 +107,7 @@ "Delay sending until docked" = "Отложить отправку данных до завершения стыковки"; /* Option to disabled Automatic Check For Updates whilst in-game [prefs.py] */ -"Disable Automatic Application Updates Check when in-game" = "Отключить автоматическое обновление приложения во время нахождения в игре."; +"Disable Automatic Application Updates Check when in-game" = "Отключить автоматическое обновление приложения во время нахождения в игре"; /* List of plugins in settings. [prefs.py] */ "Disabled Plugins" = "Отключенные плагины"; @@ -125,13 +131,13 @@ "Elite" = "Элита"; /* Section heading in settings. [edsm.py] */ -"Elite Dangerous Star Map credentials" = "Учетные данные Elite Dangerous Star Map"; +"Elite Dangerous Star Map credentials" = "Учётные данные Elite Dangerous Star Map"; /* Ranking. [stats.py] */ "Empire" = "Империя"; /* List of plugins in settings. [prefs.py] */ -"Enabled Plugins" = "Включенные плагины"; +"Enabled Plugins" = "Включённые плагины"; /* Federation rank. [stats.py] */ "Ensign" = "Энсин"; @@ -221,7 +227,7 @@ "Hotkey" = "Горячая клавиша"; /* Section heading in settings. [inara.py] */ -"Inara credentials" = "Учетные данные Inara"; +"Inara credentials" = "Учётные данные Inara"; /* Hotkey/Shortcut settings prompt on OSX. [prefs.py] */ "Keyboard shortcut" = "Сочетание клавиш"; @@ -302,7 +308,7 @@ "Open" = "Открыть"; /* Shortcut settings button on OSX. [prefs.py] */ -"Open System Preferences" = "Открыты «Системные настройки»"; +"Open System Preferences" = "Открыты «Настройки системы»"; /* Tab heading in settings. [prefs.py] */ "Output" = "Данные"; @@ -356,7 +362,7 @@ "Post Commander" = "Начальник гарнизона"; /* Ranking. [stats.py] */ -"Powerplay" = "Ранг в Игре Сил"; +"Powerplay" = "Ранг у галактических держав"; /* [prefs.py] */ "Preferences" = "Настройки"; @@ -416,13 +422,13 @@ "Semi Professional" = "Полупрофессионал"; /* [edsm.py] */ -"Send flight log and Cmdr status to EDSM" = "Отправлять данные полетного журнала и информацию статусе командира в EDSM"; +"Send flight log and Cmdr status to EDSM" = "Отправлять данные журнала полёта и информацию статусе командира в EDSM"; /* [inara.py] */ -"Send flight log and Cmdr status to Inara" = "Отправлять журнал полёта и состояние командира в Inara"; +"Send flight log and Cmdr status to Inara" = "Отправлять данные журнала полёта и информацию статусе командира в Inara"; /* Output setting. [eddn.py] */ -"Send station data to the Elite Dangerous Data Network" = "Отправлять станционную информацию на Elite Dangerous Data Network"; +"Send station data to the Elite Dangerous Data Network" = "Отправлять информацию о станциях на Elite Dangerous Data Network"; /* Output setting new in E:D 2.2. [eddn.py] */ "Send system and scan data to the Elite Dangerous Data Network" = "Отправлять данные о системе и данные сканирования в Elite Dangerous Data Network"; @@ -437,13 +443,13 @@ "Settings" = "Настройки"; /* Main window. [EDMarketConnector.py] */ -"Ship" = "Судно"; +"Ship" = "Корабль"; /* Output setting. [prefs.py] */ -"Ship loadout" = "Оборудование судна"; +"Ship loadout" = "Оборудование корабля"; /* Status dialog title. [stats.py] */ -"Ships" = "Суда"; +"Ships" = "Корабли"; /* Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis. [prefs.py] */ "Shipyard" = "Верфи"; @@ -491,7 +497,7 @@ "Update" = "Обновить данные"; /* Option to use alternate URL method on shipyard links [prefs.py] */ -"Use alternate URL method" = "Альтернативный URL(адрес)"; +"Use alternate URL method" = "Альтернативный URL (адрес)"; /* Status dialog subtitle - CR value of ship. [stats.py] */ "Value" = "Стоимость"; @@ -509,7 +515,7 @@ "Warrant Officer" = "Уорент-офицер"; /* Shouldn't happen. [EDMarketConnector.py] */ -"What are you flying?!" = "Невозможно определить тип судна!"; +"What are you flying?!" = "Невозможно определить тип корабля!"; /* Shouldn't happen. [EDMarketConnector.py] */ "Where are you?!" = "Невозможно определить местоположение!"; @@ -521,13 +527,10 @@ "Window" = "Окно"; /* [EDMarketConnector.py] */ -"You're not docked at a station!" = "Ваше судно не пристыковано к станции!"; +"You're not docked at a station!" = "Ваш корабль не пристыкован к станции!"; /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} требуется разрешение на использование глобальных сочетаний клавиш"; - -/* Label for 'UI Scaling' option [prefs.py] */ -"UI Scaling" = "Масштабирование интерфейса"; /* Label for user configured level of logging [prefs.py] */ "Log Level" = "Уровень лога"; diff --git a/L10n/sr-Latn-BA.strings b/L10n/sr-Latn-BA.strings index 0c55376b..70c6e66b 100644 --- a/L10n/sr-Latn-BA.strings +++ b/L10n/sr-Latn-BA.strings @@ -1,9 +1,6 @@ /* Label for 'UI Scaling' option [prefs.py] */ "UI Scale Percentage" = "Procenat UI skaliranja"; - -/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */ -"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 je standardna vrijednost{CR}Potreban je restart{CR}za primjenu podešavanja!"; /* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ "100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 je standardna vrijednost{CR}Potreban je restart{CR}za primjenu podešavanja!"; @@ -535,8 +532,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} traži dozvolu da koristi prečice"; - -/* Label for 'UI Scaling' option [prefs.py] */ -"UI Scaling" = "UI skaliranje"; /* Label for user configured level of logging [prefs.py] */ "Log Level" = "Log nivo"; diff --git a/L10n/sr-Latn.strings b/L10n/sr-Latn.strings index 45eacc40..39f5bce3 100644 --- a/L10n/sr-Latn.strings +++ b/L10n/sr-Latn.strings @@ -1,3 +1,9 @@ +/* Label for 'UI Scaling' option [prefs.py] */ +"UI Scale Percentage" = "Procenat UI skaliranja"; + +/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */ +"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 znači podrazumevano{CR}Restart je potreban{CR}da bi se promene primenile!"; + /* Language name */ "!Language" = "Srpski (Latinica)"; @@ -526,3 +532,5 @@ /* Shortcut settings prompt on OSX. [prefs.py] */ "{APP} needs permission to use shortcuts" = "{APP} traži dozvolu da koristi skraćenice"; +/* Label for user configured level of logging [prefs.py] */ +"Log Level" = "Log Nivo"; From ffa3e9c9a9246f8efe7692e2f98b24435b20f31b Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 18 Sep 2020 15:54:34 +0100 Subject: [PATCH 22/71] Pre-Release 4.1.0-beta5 --- ChangeLog.md | 29 +++++++++++++++++++++++++++++ config.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index fe54941b..4245028f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,35 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are in reverse chronological order (latest first). --- +Pre-Release 4.1.0-beta5 +=== + +* We are now explicitly setting a UTF8 encoding at startup. This *shouldn't* + have any side effects and has allowed us to switch to the native tkinter + file dialogues rather than some custom code. + + If you do encounter errors that might be related to this then it would be + useful to see the logging output that details the Locale settings at + various points during startup. Examples might include incorrect text being + rendered for your language when you have it set, or issues with filenames + and their content, but any of these are unlikely. + +* The error `'list' object has no attribute 'values'` should now be fixed. + +* Code dealing with Frontier's CAPI was cleaned up, so please report any + issues related to that (mostly when just docked or when you press the Update + button). + +* Extra logging added for when we process FSDJump and CarrierJump events for + EDSM. This is aimed at checking if we do have a bug with CarrierJump events, + but having FSDJump trigger the logging as well made it easier to test. + +* Default `logging` level for plugins is now DEBUG. This won't change what's + actually logged, it just ensures that everything gets through to the two + channels that then decide what is output. + +* Translations updated. Thanks again to all contributors! + Pre-Release 4.1.0-beta4 === diff --git a/config.py b/config.py index 61ec5a1a..10d4b471 100644 --- a/config.py +++ b/config.py @@ -13,7 +13,7 @@ appcmdname = 'EDMC' # appversion **MUST** follow Semantic Versioning rules: # # Major.Minor.Patch(-prerelease)(+buildmetadata) -appversion = '4.1.0-beta4' #-rc1+a872b5f' +appversion = '4.1.0-beta5' #-rc1+a872b5f' # For some things we want appversion without (possible) +build metadata appversion_nobuild = str(semantic_version.Version(appversion).truncate('prerelease')) copyright = u'© 2015-2019 Jonathan Harris, 2020 EDCD' From 0752303ab0c8f6852b23dbb013afe0176633c183 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 19 Sep 2020 09:13:30 +0100 Subject: [PATCH 23/71] Issue Template: Bug: Point out our Troubleshooting Wiki page. --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index eab87209..0dbe9be2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,6 +11,8 @@ assignees: '' [Known Issues](https://github.com/EDCD/EDMarketConnector/issues/618) in case this has already been reported.** +**Please also check if the issue is covered in our [Troubleshooting Guide](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting).** It might be something with a known work around, or where a third party (such as EDSM) is causing logging that is harmless. + **Please complete the following information:** - Version [e.g. 4.0.6 - See 'Help > About E:D Market Connector'] - OS: [e.g. Windows 10] From 10524c89dc47f83eba362a9dcf448c26831ea749 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 21 Sep 2020 09:45:03 +0100 Subject: [PATCH 24/71] Bug Report Template: Reference Debug Log File as well. --- .github/ISSUE_TEMPLATE/bug_report.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0dbe9be2..2c7e615b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,9 +7,7 @@ assignees: '' --- -**Please check the -[Known Issues](https://github.com/EDCD/EDMarketConnector/issues/618) -in case this has already been reported.** +**Please check the [Known Issues](https://github.com/EDCD/EDMarketConnector/issues/618) in case this has already been reported.** **Please also check if the issue is covered in our [Troubleshooting Guide](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting).** It might be something with a known work around, or where a third party (such as EDSM) is causing logging that is harmless. @@ -18,7 +16,11 @@ in case this has already been reported.** - OS: [e.g. Windows 10] - OS Locale: [e.g. English, French, Serbian...] - If applicable: Browser [e.g. chrome, safari] - - Please attach the file `%TEMP%\EDMarketConnector.log` from *immediately* after the bug occurs (re-running the application overwrites this file). Use the Icon that looks like `_`, `|` and `^` all in one character towards the right of the toolbar above. + - Please attach a log file: + 1. If running 4.1.0 (including betas) or later: `%TEMP%\EDMarketConnector\EDMarketConnector.log`. See [Debug Log File](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#debug-log-files). + 1. Else `%TEMP%\EDMarketConnector.log` from *immediately* after the bug occurs (re-running the application overwrites this file). + + Use the Icon that looks like `_`, `|` and `^` all in one character towards the right of the toolbar above. **Describe the bug** A clear and concise description of what the bug is. From c1584d06fa5dd4d90c471505b20ff451857bf5cb Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 21 Sep 2020 10:58:33 +0100 Subject: [PATCH 25/71] EDMarketConnector.py: Tweak setting of UTF-8. "UTF-8" is the official name, although it results in the same "utf8" afterwards in this case. --- EDMarketConnector.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index a3e46aac..ccfac5fe 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -1071,8 +1071,9 @@ sys.path: {sys.path}''' logger.debug(f'Locale LC_ALL: {locale_startup}') # Now set that same language, but utf8 encoding (it was probably cp1252 # or equivalent for other languages). - locale.setlocale(locale.LC_ALL, (locale_startup[0], 'utf8')) - log_locale('After switching to utf8 encoding (same language)') + # UTF-8, not utf8: + locale.setlocale(locale.LC_ALL, (locale_startup[0], 'UTF-8')) + log_locale('After switching to UTF-8 encoding (same language)') # TODO: unittests in place of these # logger.debug('Test from __main__') From 468912f4ec37673951e5c703cb9b5d1334868f04 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 21 Sep 2020 13:30:17 +0100 Subject: [PATCH 26/71] Attempt to select UTF-8 code page in manifest. --- EDMarketConnector.manifest | 2 ++ 1 file changed, 2 insertions(+) diff --git a/EDMarketConnector.manifest b/EDMarketConnector.manifest index 2985bbfa..ef78813a 100644 --- a/EDMarketConnector.manifest +++ b/EDMarketConnector.manifest @@ -19,6 +19,8 @@ true + + UTF-8 From 0f12959d537e8e65d8f02da0dd251d1344b61fd1 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 12:26:54 +0100 Subject: [PATCH 27/71] plugins/edsm: Also DEBUG log Location and Docked events. --- plugins/edsm.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 44a75131..895127ac 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -217,7 +217,7 @@ def credentials(cmdr): def journal_entry(cmdr, is_beta, system, station, entry, state): - if entry['event'] in ('CarrierJump', 'FSDJump'): + if entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'): logger.debug(f'''{entry["event"]} Commander: {cmdr} System: {system} @@ -300,7 +300,7 @@ entry: {entry!r}''' materials.update(transient) this.queue.put((cmdr, materials)) - if entry['event'] in ('CarrierJump', 'FSDJump'): + if entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'): logger.debug(f'''{entry["event"]} Queueing: {entry!r}''' ) @@ -359,11 +359,11 @@ def worker(): retrying = 0 while retrying < 3: try: - if item and entry['event'] in ('CarrierJump', 'FSDJump'): + if item and entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'): logger.debug(f'{entry["event"]}') if item and entry['event'] not in this.discardedEvents: - if entry['event'] in ('CarrierJump', 'FSDJump'): + if entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'): logger.debug(f'{entry["event"]} event not in discarded list') pending.append(entry) @@ -377,7 +377,7 @@ def worker(): pending = [x for x in pending if x['event'] not in this.discardedEvents] # Filter out unwanted events if should_send(pending): - if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump')]): + if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): logger.debug('CarrierJump (or FSDJump) in pending and it passed should_send()') (username, apikey) = credentials(cmdr) @@ -389,7 +389,7 @@ def worker(): 'message': json.dumps(pending, ensure_ascii=False).encode('utf-8'), } - if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump')]): + if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): data_elided = data.copy() data_elided['apiKey'] = '' logger.debug(f'''CarrierJump (or FSDJump): Attempting API call From 37c53e233e02cc7c3c3f3bba4dc293c1f10404b4 Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 18 Aug 2020 10:19:08 +0200 Subject: [PATCH 28/71] autoformatted code with autopep8 --- plugins/edsm.py | 69 ++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 895127ac..7ad6f7c2 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -31,17 +31,17 @@ EDSM_POLL = 0.1 _TIMEOUT = 20 -this = sys.modules[__name__] # For holding module globals +this = sys.modules[__name__] # For holding module globals this.session = requests.Session() this.queue = Queue() # Items to be sent to EDSM by worker thread -this.discardedEvents = [] # List discarded events from EDSM +this.discardedEvents = [] # List discarded events from EDSM this.lastlookup = False # whether the last lookup succeeded # Game state this.multicrew = False # don't send captain's ship info to EDSM while on a crew this.coordinates = None this.newgame = False # starting up - batch initial burst of events -this.newgame_docked = False # starting up while docked +this.newgame_docked = False # starting up while docked this.navbeaconscan = 0 # batch up burst of Scan events after NavBeaconScan this.system_link = None this.system = None @@ -53,8 +53,6 @@ this.station_marketid = None # Frontier MarketID STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7 - - # Main window clicks def system_url(system_name): if this.system_address: @@ -65,6 +63,7 @@ def system_url(system_name): return '' + def station_url(system_name, station_name): if system_name and station_name: return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemName={system_name}&stationName={station_name}') @@ -80,10 +79,10 @@ def station_url(system_name, station_name): def plugin_start3(plugin_dir): # Can't be earlier since can only call PhotoImage after window is created - this._IMG_KNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7rMpvNV4q932VSuRiPjQQAOw==') # green circle - this._IMG_UNKNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kCADs=') # red circle - this._IMG_NEW = tk.PhotoImage(data = 'R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXOPvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//jd//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/slv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP///////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xHJk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+GBAcUCIIBBDSYYGiAAUMALFR6FAgAOw==') - this._IMG_ERROR = tk.PhotoImage(data = 'R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2LplEAADs=') # BBC Mode 5 '?' + this._IMG_KNOWN = tk.PhotoImage(data='R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7rMpvNV4q932VSuRiPjQQAOw==') # green circle + this._IMG_UNKNOWN = tk.PhotoImage(data='R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kCADs=') # red circle + this._IMG_NEW = tk.PhotoImage(data='R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXOPvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//jd//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/slv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP///////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xHJk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+GBAcUCIIBBDSYYGiAAUMALFR6FAgAOw==') + this._IMG_ERROR = tk.PhotoImage(data='R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2LplEAADs=') # BBC Mode 5 '?' # Migrate old settings if not config.get('edsm_cmdrs'): @@ -102,17 +101,19 @@ def plugin_start3(plugin_dir): config.delete('edsm_autoopen') config.delete('edsm_historical') - this.thread = Thread(target = worker, name = 'EDSM worker') + this.thread = Thread(target=worker, name='EDSM worker') this.thread.daemon = True this.thread.start() return 'EDSM' + def plugin_app(parent): this.system_link = parent.children['system'] # system label in main window this.system_link.bind_all('<>', update_status) this.station_link = parent.children['station'] # station label in main window + def plugin_stop(): # Signal thread to close and wait for it this.queue.put(None) @@ -121,35 +122,39 @@ def plugin_stop(): # Suppress 'Exception ignored in: ' errors this._IMG_KNOWN = this._IMG_UNKNOWN = this._IMG_NEW = this._IMG_ERROR = None + def plugin_prefs(parent, cmdr, is_beta): PADX = 10 - BUTTONX = 12 # indent Checkbuttons and Radiobuttons + BUTTONX = 12 # indent Checkbuttons and Radiobuttons PADY = 2 # close spacing frame = nb.Frame(parent) frame.columnconfigure(1, weight=1) - HyperlinkLabel(frame, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate - this.log = tk.IntVar(value = config.getint('edsm_out') and 1) - this.log_button = nb.Checkbutton(frame, text=_('Send flight log and Cmdr status to EDSM'), variable=this.log, command=prefsvarchanged) - this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) + HyperlinkLabel(frame, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), + url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate + this.log = tk.IntVar(value=config.getint('edsm_out') and 1) + this.log_button = nb.Checkbutton(frame, text=_('Send flight log and Cmdr status to EDSM'), + variable=this.log, command=prefsvarchanged) + this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5, 0), sticky=tk.W) - nb.Label(frame).grid(sticky=tk.W) # big spacer - this.label = HyperlinkLabel(frame, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings + nb.Label(frame).grid(sticky=tk.W) # big spacer + this.label = HyperlinkLabel(frame, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget( + 'background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings this.label.grid(columnspan=2, padx=PADX, sticky=tk.W) - this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window + this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window this.cmdr_label.grid(row=10, padx=PADX, sticky=tk.W) this.cmdr_text = nb.Label(frame) this.cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W) - this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting + this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting this.user_label.grid(row=11, padx=PADX, sticky=tk.W) this.user = nb.Entry(frame) this.user.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) - this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting + this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting this.apikey_label.grid(row=12, padx=PADX, sticky=tk.W) this.apikey = nb.Entry(frame) this.apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW) @@ -158,6 +163,7 @@ def plugin_prefs(parent, cmdr, is_beta): return frame + def prefs_cmdr_changed(cmdr, is_beta): this.log_button['state'] = cmdr and not is_beta and tk.NORMAL or tk.DISABLED this.user['state'] = tk.NORMAL @@ -174,9 +180,11 @@ def prefs_cmdr_changed(cmdr, is_beta): this.cmdr_text['text'] = _('None') # No hotkey/shortcut currently defined this.label['state'] = this.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.apikey['state'] = cmdr and not is_beta and this.log.get() and tk.NORMAL or tk.DISABLED + def prefsvarchanged(): this.label['state'] = this.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.apikey['state'] = this.log.get() and this.log_button['state'] or tk.DISABLED + def prefs_changed(cmdr, is_beta): config.set('edsm_out', this.log.get()) @@ -293,9 +301,9 @@ entry: {entry!r}''' materials = { 'timestamp': entry['timestamp'], 'event': 'Materials', - 'Raw': [ { 'Name': k, 'Count': v } for k,v in state['Raw'].items() ], - 'Manufactured': [ { 'Name': k, 'Count': v } for k,v in state['Manufactured'].items() ], - 'Encoded': [ { 'Name': k, 'Count': v } for k,v in state['Encoded'].items() ], + 'Raw': [{'Name': k, 'Count': v} for k, v in state['Raw'].items()], + 'Manufactured': [{'Name': k, 'Count': v} for k, v in state['Manufactured'].items()], + 'Encoded': [{'Name': k, 'Count': v} for k, v in state['Encoded'].items()], } materials.update(transient) this.queue.put((cmdr, materials)) @@ -346,7 +354,7 @@ def cmdr_data(data, is_beta): # Worker thread def worker(): - pending = [] # Unsent events + pending = [] # Unsent events closing = False while True: @@ -354,7 +362,7 @@ def worker(): if item: (cmdr, entry) = item else: - closing = True # Try to send any unsent events before we close + closing = True # Try to send any unsent events before we close retrying = 0 while retrying < 3: @@ -372,9 +380,10 @@ def worker(): r = this.session.get('https://www.edsm.net/api-journal-v1/discard', timeout=_TIMEOUT) r.raise_for_status() this.discardedEvents = set(r.json()) - this.discardedEvents.discard('Docked') # should_send() assumes that we send 'Docked' events + this.discardedEvents.discard('Docked') # should_send() assumes that we send 'Docked' events assert this.discardedEvents # wouldn't expect this to be empty - pending = [x for x in pending if x['event'] not in this.discardedEvents] # Filter out unwanted events + pending = [x for x in pending if x['event'] + not in this.discardedEvents] # Filter out unwanted events if should_send(pending): if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): @@ -450,9 +459,9 @@ def should_send(entries): return True elif this.newgame: pass - elif entry['event'] not in ['CommunityGoal', # Spammed periodically + elif entry['event'] not in ['CommunityGoal', # Spammed periodically 'ModuleBuy', 'ModuleSell', 'ModuleSwap', # will be shortly followed by "Loadout" - 'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap']: # " + 'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap']: # " return True return False @@ -469,7 +478,7 @@ def edsm_notify_system(reply): if not reply: this.system_link['image'] = this._IMG_ERROR plug.show_error(_("Error: Can't connect to EDSM")) - elif reply['msgnum'] // 100 not in (1,4): + elif reply['msgnum'] // 100 not in (1, 4): this.system_link['image'] = this._IMG_ERROR plug.show_error(_('Error: EDSM {MSG}').format(MSG=reply['msg'])) elif reply.get('systemCreated'): From d2a1f09fac98aeaaec5361e00d6bc11b49db987d Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 18 Aug 2020 10:19:36 +0200 Subject: [PATCH 29/71] sorted imports --- plugins/edsm.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 7ad6f7c2..9edb78a5 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -12,18 +12,21 @@ # text is always fired. i.e. CAPI cmdr_data() processing. import json -import requests +import logging import sys +import tkinter as tk +import urllib.error +import urllib.parse +import urllib.request from queue import Queue from threading import Thread -import logging -import tkinter as tk -from ttkHyperlinkLabel import HyperlinkLabel +import requests + import myNotebook as nb # noqa: N813 - -from config import appname, applongname, appversion, config import plug +from config import applongname, appname, appversion, config +from ttkHyperlinkLabel import HyperlinkLabel logger = logging.getLogger(appname) From 743984eb11052a7c6fc637bf9924f75956aa9a3d Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 18 Aug 2020 10:22:59 +0200 Subject: [PATCH 30/71] Added fake definition for _ _ is added by magic in the plugin loading system, which means there are no imports to indicate its existence to static analysis tools. --- plugins/edsm.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/edsm.py b/plugins/edsm.py index 9edb78a5..4023f843 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -20,6 +20,7 @@ import urllib.parse import urllib.request from queue import Queue from threading import Thread +from typing import TYPE_CHECKING, Any import requests @@ -28,6 +29,10 @@ import plug from config import applongname, appname, appversion, config from ttkHyperlinkLabel import HyperlinkLabel +if TYPE_CHECKING: + def _(x: str) -> str: + return x + logger = logging.getLogger(appname) EDSM_POLL = 0.1 From 199e76701df1b540e1bb88f343e1190975dedf17 Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 18 Aug 2020 10:28:32 +0200 Subject: [PATCH 31/71] Added line breaks around scope changes --- plugins/edsm.py | 46 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 4023f843..f5438b6c 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -39,7 +39,7 @@ EDSM_POLL = 0.1 _TIMEOUT = 20 -this = sys.modules[__name__] # For holding module globals +this: Any = sys.modules[__name__] # For holding module globals this.session = requests.Session() this.queue = Queue() # Items to be sent to EDSM by worker thread this.discardedEvents = [] # List discarded events from EDSM @@ -97,15 +97,19 @@ def plugin_start3(plugin_dir): if isinstance(config.get('cmdrs'), list) and config.get('edsm_usernames') and config.get('edsm_apikeys'): # Migrate <= 2.34 settings config.set('edsm_cmdrs', config.get('cmdrs')) + elif config.get('edsm_cmdrname'): # Migrate <= 2.25 settings. edsm_cmdrs is unknown at this time config.set('edsm_usernames', [config.get('edsm_cmdrname') or '']) config.set('edsm_apikeys', [config.get('edsm_apikey') or '']) + config.delete('edsm_cmdrname') config.delete('edsm_apikey') + if config.getint('output') & 256: # Migrate <= 2.34 setting config.set('edsm_out', 1) + config.delete('edsm_autoopen') config.delete('edsm_historical') @@ -140,16 +144,13 @@ def plugin_prefs(parent, cmdr, is_beta): frame = nb.Frame(parent) frame.columnconfigure(1, weight=1) - HyperlinkLabel(frame, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), - url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate + HyperlinkLabel(frame, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate this.log = tk.IntVar(value=config.getint('edsm_out') and 1) - this.log_button = nb.Checkbutton(frame, text=_('Send flight log and Cmdr status to EDSM'), - variable=this.log, command=prefsvarchanged) + this.log_button = nb.Checkbutton(frame, text=_('Send flight log and Cmdr status to EDSM'), variable=this.log, command=prefsvarchanged) this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5, 0), sticky=tk.W) nb.Label(frame).grid(sticky=tk.W) # big spacer - this.label = HyperlinkLabel(frame, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget( - 'background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings + this.label = HyperlinkLabel(frame, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings this.label.grid(columnspan=2, padx=PADX, sticky=tk.W) this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window @@ -181,11 +182,14 @@ def prefs_cmdr_changed(cmdr, is_beta): if cmdr: this.cmdr_text['text'] = cmdr + (is_beta and ' [Beta]' or '') cred = credentials(cmdr) + if cred: this.user.insert(0, cred[0]) this.apikey.insert(0, cred[1]) + else: this.cmdr_text['text'] = _('None') # No hotkey/shortcut currently defined + this.label['state'] = this.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.apikey['state'] = cmdr and not is_beta and this.log.get() and tk.NORMAL or tk.DISABLED @@ -206,10 +210,12 @@ def prefs_changed(cmdr, is_beta): usernames[idx] = this.user.get().strip() apikeys.extend([''] * (1 + idx - len(apikeys))) apikeys[idx] = this.apikey.get().strip() + else: config.set('edsm_cmdrs', cmdrs + [cmdr]) usernames.append(this.user.get().strip()) apikeys.append(this.apikey.get().strip()) + config.set('edsm_usernames', usernames) config.set('edsm_apikeys', apikeys) @@ -228,6 +234,7 @@ def credentials(cmdr): if cmdr in cmdrs and config.get('edsm_usernames') and config.get('edsm_apikeys'): idx = cmdrs.index(cmdr) return (config.get('edsm_usernames')[idx], config.get('edsm_apikeys')[idx]) + else: return None @@ -275,6 +282,7 @@ entry: {entry!r}''' this.multicrew = bool(state['Role']) if 'StarPos' in entry: this.coordinates = entry['StarPos'] + elif entry['event'] == 'LoadGame': this.coordinates = None @@ -282,14 +290,17 @@ entry: {entry!r}''' this.newgame = True this.newgame_docked = False this.navbeaconscan = 0 + elif entry['event'] == 'StartUp': this.newgame = False this.newgame_docked = False this.navbeaconscan = 0 + elif entry['event'] == 'Location': this.newgame = True this.newgame_docked = entry.get('Docked', False) this.navbeaconscan = 0 + elif entry['event'] == 'NavBeaconScan': this.navbeaconscan = entry['NumBodies'] @@ -302,6 +313,7 @@ entry: {entry!r}''' '_stationName': station, '_shipId': state['ShipID'], } + entry.update(transient) if entry['event'] == 'LoadGame': @@ -330,6 +342,7 @@ def cmdr_data(data, is_beta): # Always store initially, even if we're not the *current* system provider. if not this.station_marketid: this.station_marketid = data['commander']['docked'] and data['lastStarport']['id'] + # Only trust CAPI if these aren't yet set this.system = this.system or data['lastSystem']['name'] this.station = this.station or data['commander']['docked'] and data['lastStarport']['name'] @@ -340,11 +353,14 @@ def cmdr_data(data, 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.system_link.update_idletasks() + if config.get('station_provider') == 'EDSM': if data['commander']['docked']: this.station_link['text'] = this.station + elif data['lastStarport']['name'] and data['lastStarport']['name'] != "": this.station_link['text'] = STATION_UNDOCKED + else: this.station_link['text'] = '' @@ -361,7 +377,6 @@ def cmdr_data(data, is_beta): # Worker thread def worker(): - pending = [] # Unsent events closing = False @@ -409,9 +424,7 @@ def worker(): if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): data_elided = data.copy() data_elided['apiKey'] = '' - logger.debug(f'''CarrierJump (or FSDJump): Attempting API call -data: {data_elided!r}''' - ) + logger.debug(f'CarrierJump (or FSDJump): Attempting API call\ndata: {data_elided!r}') r = this.session.post('https://www.edsm.net/api-journal-v1', data=data, timeout=_TIMEOUT) r.raise_for_status() reply = r.json() @@ -423,6 +436,7 @@ data: {data_elided!r}''' if msgnum // 100 == 2: logger.warning(f'EDSM\t{msgnum} {msg}\t{json.dumps(pending, separators = (",", ": "))}') plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg)) + else: for e, r in zip(pending, reply['events']): if not closing and e['event'] in ['StartUp', 'Location', 'FSDJump', 'CarrierJump']: @@ -430,15 +444,18 @@ data: {data_elided!r}''' this.lastlookup = r # calls update_status in main thread this.system_link.event_generate('<>', when="tail") + elif r['msgnum'] // 100 != 1: logger.warning(f'EDSM\t{r["msgnum"]} {r["msg"]}\t' f'{json.dumps(e, separators = (",", ": "))}') + pending = [] break except Exception as e: logger.debug('Sending API events', exc_info=e) retrying += 1 + else: plug.show_error(_("Error: Can't connect to EDSM")) @@ -455,6 +472,7 @@ def should_send(entries): this.navbeaconscan -= 1 if this.navbeaconscan: return False + else: assert(False) this.navbeaconscan = 0 @@ -465,12 +483,15 @@ def should_send(entries): this.newgame = False this.newgame_docked = False return True + elif this.newgame: pass + elif entry['event'] not in ['CommunityGoal', # Spammed periodically 'ModuleBuy', 'ModuleSell', 'ModuleSwap', # will be shortly followed by "Loadout" 'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap']: # " return True + return False @@ -486,10 +507,13 @@ def edsm_notify_system(reply): if not reply: this.system_link['image'] = this._IMG_ERROR plug.show_error(_("Error: Can't connect to EDSM")) + elif reply['msgnum'] // 100 not in (1, 4): this.system_link['image'] = this._IMG_ERROR plug.show_error(_('Error: EDSM {MSG}').format(MSG=reply['msg'])) + elif reply.get('systemCreated'): this.system_link['image'] = this._IMG_NEW + else: this.system_link['image'] = this._IMG_KNOWN From 14617e72da9ea370255dceef7a79204c4d181878 Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 18 Aug 2020 10:52:44 +0200 Subject: [PATCH 32/71] cleaned up overlong lines --- plugins/edsm.py | 117 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 19 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index f5438b6c..4b40d14e 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -59,6 +59,31 @@ this.station_link = None this.station = None this.station_marketid = None # Frontier MarketID STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7 +__cleanup = str.maketrans({' ': None, '\n': None}) +IMG_KNOWN_B64 = """ +R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7r +MpvNV4q932VSuRiPjQQAOw== +""".translate(__cleanup) + +IMG_UNKNOWN_B64 = """ +R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kC +ADs= +""".translate(__cleanup) + +IMG_NEW_B64 = """ +R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXO +PvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//j +d//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/s +lv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP////////////// +/////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xH +Jk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+G +BAcUCIIBBDSYYGiAAUMALFR6FAgAOw== +""".translate(__cleanup) + +IMG_ERR_B64 = """ +R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2L +plEAADs= +""".translate(__cleanup) # Main window clicks @@ -87,10 +112,10 @@ def station_url(system_name, station_name): def plugin_start3(plugin_dir): # Can't be earlier since can only call PhotoImage after window is created - this._IMG_KNOWN = tk.PhotoImage(data='R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7rMpvNV4q932VSuRiPjQQAOw==') # green circle - this._IMG_UNKNOWN = tk.PhotoImage(data='R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kCADs=') # red circle - this._IMG_NEW = tk.PhotoImage(data='R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXOPvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//jd//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/slv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP///////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xHJk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+GBAcUCIIBBDSYYGiAAUMALFR6FAgAOw==') - this._IMG_ERROR = tk.PhotoImage(data='R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2LplEAADs=') # BBC Mode 5 '?' + this._IMG_KNOWN = tk.PhotoImage(data=IMG_KNOWN_B64) # green circle + this._IMG_UNKNOWN = tk.PhotoImage(data=IMG_UNKNOWN_B64) # red circle + this._IMG_NEW = tk.PhotoImage(data=IMG_NEW_B64) + this._IMG_ERROR = tk.PhotoImage(data=IMG_ERR_B64) # BBC Mode 5 '?' # Migrate old settings if not config.get('edsm_cmdrs'): @@ -136,7 +161,6 @@ def plugin_stop(): def plugin_prefs(parent, cmdr, is_beta): - PADX = 10 BUTTONX = 12 # indent Checkbuttons and Radiobuttons PADY = 2 # close spacing @@ -144,13 +168,31 @@ def plugin_prefs(parent, cmdr, is_beta): frame = nb.Frame(parent) frame.columnconfigure(1, weight=1) - HyperlinkLabel(frame, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate + HyperlinkLabel( + frame, + text='Elite Dangerous Star Map', + background=nb.Label().cget('background'), + url='https://www.edsm.net/', + underline=True + ).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate + this.log = tk.IntVar(value=config.getint('edsm_out') and 1) - this.log_button = nb.Checkbutton(frame, text=_('Send flight log and Cmdr status to EDSM'), variable=this.log, command=prefsvarchanged) + this.log_button = nb.Checkbutton( + frame, text=_('Send flight log and Cmdr status to EDSM'), variable=this.log, command=prefsvarchanged + ) + this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5, 0), sticky=tk.W) nb.Label(frame).grid(sticky=tk.W) # big spacer - this.label = HyperlinkLabel(frame, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings + # Section heading in settings + this.label = HyperlinkLabel( + frame, + text=_('Elite Dangerous Star Map credentials'), + background=nb.Label().cget('background'), + url='https://www.edsm.net/settings/api', + underline=True + ) + this.label.grid(columnspan=2, padx=PADX, sticky=tk.W) this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window @@ -190,11 +232,34 @@ def prefs_cmdr_changed(cmdr, is_beta): else: this.cmdr_text['text'] = _('None') # No hotkey/shortcut currently defined - this.label['state'] = this.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.apikey['state'] = cmdr and not is_beta and this.log.get() and tk.NORMAL or tk.DISABLED + to_set = tk.DISABLED + if cmdr and not is_beta and this.log.get(): + to_set = tk.NORMAL + + set_prefs_ui_states(to_set) def prefsvarchanged(): - this.label['state'] = this.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.apikey['state'] = this.log.get() and this.log_button['state'] or tk.DISABLED + to_set = tk.DISABLED + if this.log.get(): + to_set = this.log_button['state'] + + set_prefs_ui_states(to_set) + + +def set_prefs_ui_states(state: str) -> None: + """ + Set the state of various config UI entries + + :param state: the state to set each entry to + """ + this.label['state'] = state + this.cmdr_label['state'] = state + this.cmdr_text['state'] = state + this.user_label['state'] = state + this.user['state'] = state + this.apikey_label['state'] = state + this.apikey['state'] = state def prefs_changed(cmdr, is_beta): @@ -269,7 +334,15 @@ entry: {entry!r}''' this.station_marketid = None if config.get('station_provider') == 'EDSM': - this.station_link['text'] = this.station or (this.system_population and this.system_population > 0 and STATION_UNDOCKED or '') + to_set = this.station + if not this.station: + if this.system_population and this.system_population > 0: + to_set = STATION_UNDOCKED + + else: + to_set = '' + + this.station_link['text'] = to_set this.station_link['url'] = station_url(this.system, this.station) this.station_link.update_idletasks() @@ -305,7 +378,10 @@ entry: {entry!r}''' this.navbeaconscan = entry['NumBodies'] # Send interesting events to EDSM - if config.getint('edsm_out') and not is_beta and not this.multicrew and credentials(cmdr) and entry['event'] not in this.discardedEvents: + if ( + config.getint('edsm_out') and not is_beta and not this.multicrew and credentials(cmdr) and + entry['event'] not in this.discardedEvents + ): # Introduce transient states into the event transient = { '_systemName': system, @@ -405,8 +481,8 @@ def worker(): this.discardedEvents = set(r.json()) this.discardedEvents.discard('Docked') # should_send() assumes that we send 'Docked' events assert this.discardedEvents # wouldn't expect this to be empty - pending = [x for x in pending if x['event'] - not in this.discardedEvents] # Filter out unwanted events + # Filter out unwanted events + pending = [x for x in pending if x['event'] not in this.discardedEvents] if should_send(pending): if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): @@ -487,21 +563,24 @@ def should_send(entries): elif this.newgame: pass - elif entry['event'] not in ['CommunityGoal', # Spammed periodically - 'ModuleBuy', 'ModuleSell', 'ModuleSwap', # will be shortly followed by "Loadout" - 'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap']: # " + elif entry['event'] not in [ + 'CommunityGoal', # Spammed periodically + 'ModuleBuy', 'ModuleSell', 'ModuleSwap', # will be shortly followed by "Loadout" + 'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap']: # " return True return False -# Call edsm_notify_system() in this and other interested plugins with EDSM's response to a 'StartUp', 'Location', 'FSDJump' or 'CarrierJump' event +# Call edsm_notify_system() in this and other interested plugins with EDSM's response to a 'StartUp', 'Location', +# 'FSDJump' or 'CarrierJump' event def update_status(event=None): for plugin in plug.provides('edsm_notify_system'): plug.invoke(plugin, None, 'edsm_notify_system', this.lastlookup) -# Called with EDSM's response to a 'StartUp', 'Location', 'FSDJump' or 'CarrierJump' event. https://www.edsm.net/en/api-journal-v1 +# Called with EDSM's response to a 'StartUp', 'Location', 'FSDJump' or 'CarrierJump' event. +# https://www.edsm.net/en/api-journal-v1 # msgnum: 1xx = OK, 2xx = fatal error, 3xx = error, 4xx = ignorable errors. def edsm_notify_system(reply): if not reply: From 81c977d0da729a5ab4f93ce1a448e00d37a2174b Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 18 Aug 2020 11:08:35 +0200 Subject: [PATCH 33/71] Added type annotations to functions --- plugins/edsm.py | 80 +++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 4b40d14e..5496d05f 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -20,7 +20,7 @@ import urllib.parse import urllib.request from queue import Queue from threading import Thread -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, List, Mapping, MutableMapping, Optional, Tuple import requests @@ -40,24 +40,24 @@ _TIMEOUT = 20 this: Any = sys.modules[__name__] # For holding module globals -this.session = requests.Session() -this.queue = Queue() # Items to be sent to EDSM by worker thread -this.discardedEvents = [] # List discarded events from EDSM -this.lastlookup = False # whether the last lookup succeeded +this.session: requests.Session = requests.Session() +this.queue: Queue = Queue() # Items to be sent to EDSM by worker thread +this.discardedEvents: List[str] = [] # List discarded events from EDSM +this.lastlookup: bool = False # whether the last lookup succeeded # Game state -this.multicrew = False # don't send captain's ship info to EDSM while on a crew -this.coordinates = None -this.newgame = False # starting up - batch initial burst of events -this.newgame_docked = False # starting up while docked -this.navbeaconscan = 0 # batch up burst of Scan events after NavBeaconScan -this.system_link = None -this.system = None -this.system_address = None # Frontier SystemAddress -this.system_population = None -this.station_link = None -this.station = None -this.station_marketid = None # Frontier MarketID +this.multicrew: bool = False # don't send captain's ship info to EDSM while on a crew +this.coordinates: Optional[Tuple[int, int, int]] = None +this.newgame: bool = False # starting up - batch initial burst of events +this.newgame_docked: bool = False # starting up while docked +this.navbeaconscan: int = 0 # batch up burst of Scan events after NavBeaconScan +this.system_link: tk.Tk = None +this.system: tk.Tk = None +this.system_address: Optional[int] = None # Frontier SystemAddress +this.system_population: Optional[int] = None +this.station_link: tk.Tk = None +this.station: Optional[str] = None +this.station_marketid: Optional[int] = None # Frontier MarketID STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7 __cleanup = str.maketrans({' ': None, '\n': None}) IMG_KNOWN_B64 = """ @@ -87,7 +87,7 @@ plEAADs= # Main window clicks -def system_url(system_name): +def system_url(system_name: str) -> str: if this.system_address: return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemID64={this.system_address}') @@ -97,7 +97,7 @@ def system_url(system_name): return '' -def station_url(system_name, station_name): +def station_url(system_name: str, station_name: str) -> str: if system_name and station_name: return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemName={system_name}&stationName={station_name}') @@ -110,7 +110,7 @@ def station_url(system_name, station_name): return '' -def plugin_start3(plugin_dir): +def plugin_start3(plugin_dir: str) -> str: # 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 @@ -145,13 +145,13 @@ def plugin_start3(plugin_dir): return 'EDSM' -def plugin_app(parent): +def plugin_app(parent: tk.Tk) -> None: this.system_link = parent.children['system'] # system label in main window this.system_link.bind_all('<>', update_status) this.station_link = parent.children['station'] # station label in main window -def plugin_stop(): +def plugin_stop() -> None: # Signal thread to close and wait for it this.queue.put(None) this.thread.join() @@ -160,7 +160,7 @@ def plugin_stop(): this._IMG_KNOWN = this._IMG_UNKNOWN = this._IMG_NEW = this._IMG_ERROR = None -def plugin_prefs(parent, cmdr, is_beta): +def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: PADX = 10 BUTTONX = 12 # indent Checkbuttons and Radiobuttons PADY = 2 # close spacing @@ -172,7 +172,7 @@ def plugin_prefs(parent, cmdr, is_beta): frame, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), - url='https://www.edsm.net/', + url='https://www.edsm.net/', underline=True ).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate @@ -215,7 +215,7 @@ def plugin_prefs(parent, cmdr, is_beta): return frame -def prefs_cmdr_changed(cmdr, is_beta): +def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: this.log_button['state'] = cmdr and not is_beta and tk.NORMAL or tk.DISABLED this.user['state'] = tk.NORMAL this.user.delete(0, tk.END) @@ -239,7 +239,7 @@ def prefs_cmdr_changed(cmdr, is_beta): set_prefs_ui_states(to_set) -def prefsvarchanged(): +def prefsvarchanged() -> None: to_set = tk.DISABLED if this.log.get(): to_set = this.log_button['state'] @@ -262,13 +262,13 @@ def set_prefs_ui_states(state: str) -> None: this.apikey['state'] = state -def prefs_changed(cmdr, is_beta): +def prefs_changed(cmdr: str, is_beta: bool) -> None: config.set('edsm_out', this.log.get()) if cmdr and not is_beta: cmdrs = config.get('edsm_cmdrs') - usernames = config.get('edsm_usernames') or [] - apikeys = config.get('edsm_apikeys') or [] + usernames: List[str] = config.get('edsm_usernames') or [] + apikeys: List[str] = config.get('edsm_apikeys') or [] if cmdr in cmdrs: idx = cmdrs.index(cmdr) usernames.extend([''] * (1 + idx - len(usernames))) @@ -285,7 +285,7 @@ def prefs_changed(cmdr, is_beta): config.set('edsm_apikeys', apikeys) -def credentials(cmdr): +def credentials(cmdr: str) -> Optional[Tuple[str, str]]: # Credentials for cmdr if not cmdr: return None @@ -304,7 +304,9 @@ def credentials(cmdr): return None -def journal_entry(cmdr, is_beta, system, station, entry, state): +def journal_entry( + cmdr: str, is_beta: bool, system: str, station: str, entry: MutableMapping[str, Any], state: Mapping[str, Any] +) -> None: if entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'): logger.debug(f'''{entry["event"]} Commander: {cmdr} @@ -313,7 +315,6 @@ Station: {station} state: {state!r} entry: {entry!r}''' ) - # Always update our system address even if we're not currently the provider for system or station, but dont update # on events that contain "future" data, such as FSDTarget if entry['event'] in ('Location', 'Docked', 'CarrierJump', 'FSDJump'): @@ -412,7 +413,7 @@ Queueing: {entry!r}''' # Update system data -def cmdr_data(data, is_beta): +def cmdr_data(data: Mapping[str, Any], is_beta: bool) -> None: system = data['lastSystem']['name'] # Always store initially, even if we're not the *current* system provider. @@ -452,12 +453,12 @@ def cmdr_data(data, is_beta): # Worker thread -def worker(): +def worker() -> None: pending = [] # Unsent events closing = False while True: - item = this.queue.get() + item: Optional[Tuple[str, Mapping[str, Any]]] = this.queue.get() if item: (cmdr, entry) = item else: @@ -466,6 +467,7 @@ def worker(): retrying = 0 while retrying < 3: try: + # TODO: Technically entry can be unbound here. if item and entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'): logger.debug(f'{entry["event"]}') @@ -488,7 +490,7 @@ def worker(): if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): logger.debug('CarrierJump (or FSDJump) in pending and it passed should_send()') - (username, apikey) = credentials(cmdr) + (username, apikey) = credentials(cmdr) # TODO: This raises if credentials returns None data = { 'commanderName': username.encode('utf-8'), 'apiKey': apikey, @@ -540,7 +542,7 @@ def worker(): # Whether any of the entries should be sent immediately -def should_send(entries): +def should_send(entries: List[Mapping[str, Any]]) -> bool: # batch up burst of Scan events after NavBeaconScan if this.navbeaconscan: @@ -574,7 +576,7 @@ def should_send(entries): # Call edsm_notify_system() in this and other interested plugins with EDSM's response to a 'StartUp', 'Location', # 'FSDJump' or 'CarrierJump' event -def update_status(event=None): +def update_status(event=None) -> None: for plugin in plug.provides('edsm_notify_system'): plug.invoke(plugin, None, 'edsm_notify_system', this.lastlookup) @@ -582,7 +584,7 @@ def update_status(event=None): # Called with EDSM's response to a 'StartUp', 'Location', 'FSDJump' or 'CarrierJump' event. # https://www.edsm.net/en/api-journal-v1 # msgnum: 1xx = OK, 2xx = fatal error, 3xx = error, 4xx = ignorable errors. -def edsm_notify_system(reply): +def edsm_notify_system(reply: Mapping[str, Any]) -> None: if not reply: this.system_link['image'] = this._IMG_ERROR plug.show_error(_("Error: Can't connect to EDSM")) From 20357b3debb0314ef19284666951b3e65e58f1e2 Mon Sep 17 00:00:00 2001 From: A_D Date: Wed, 19 Aug 2020 04:39:13 +0200 Subject: [PATCH 34/71] replaced modulo formatting with fstrings --- plugins/edsm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 5496d05f..aca3c623 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -543,7 +543,6 @@ def worker() -> None: # Whether any of the entries should be sent immediately def should_send(entries: List[Mapping[str, Any]]) -> bool: - # batch up burst of Scan events after NavBeaconScan if this.navbeaconscan: if entries and entries[-1]['event'] == 'Scan': From 2c2ade05e0fd9bb5d06ea7c66cdd25f94728d8cd Mon Sep 17 00:00:00 2001 From: A_D Date: Wed, 19 Aug 2020 12:25:48 +0200 Subject: [PATCH 35/71] fixed some naming --- plugins/edsm.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index aca3c623..766c2dd3 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -161,9 +161,9 @@ def plugin_stop() -> None: def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: - PADX = 10 - BUTTONX = 12 # indent Checkbuttons and Radiobuttons - PADY = 2 # close spacing + PADX = 10 # noqa: N806 + BUTTONX = 12 # indent Checkbuttons and Radiobuttons # noqa: N806 + PADY = 2 # close spacing # noqa: N806 frame = nb.Frame(parent) frame.columnconfigure(1, weight=1) @@ -222,7 +222,7 @@ def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: this.apikey['state'] = tk.NORMAL this.apikey.delete(0, tk.END) if cmdr: - this.cmdr_text['text'] = cmdr + (is_beta and ' [Beta]' or '') + this.cmdr_text['text'] = f'{cmdr}{" [Beta]" if is_beta else ""}' cred = credentials(cmdr) if cred: @@ -266,9 +266,10 @@ def prefs_changed(cmdr: str, is_beta: bool) -> None: config.set('edsm_out', this.log.get()) if cmdr and not is_beta: - cmdrs = config.get('edsm_cmdrs') - usernames: List[str] = config.get('edsm_usernames') or [] - apikeys: List[str] = config.get('edsm_apikeys') or [] + # TODO: remove this when config is rewritten. + cmdrs: List[str] = list(config.get('edsm_cmdrs') or []) + usernames: List[str] = list(config.get('edsm_usernames') or []) + apikeys: List[str] = list(config.get('edsm_apikeys') or []) if cmdr in cmdrs: idx = cmdrs.index(cmdr) usernames.extend([''] * (1 + idx - len(usernames))) @@ -344,7 +345,7 @@ entry: {entry!r}''' to_set = '' this.station_link['text'] = to_set - this.station_link['url'] = station_url(this.system, this.station) + this.station_link['url'] = station_url(this.system, str(this.station)) this.station_link.update_idletasks() # Update display of 'EDSM Status' image @@ -506,13 +507,13 @@ def worker() -> None: r = this.session.post('https://www.edsm.net/api-journal-v1', data=data, timeout=_TIMEOUT) r.raise_for_status() reply = r.json() - (msgnum, msg) = reply['msgnum'], reply['msg'] + (msg_num, msg) = reply['msgnum'], reply['msg'] # 1xx = OK # 2xx = fatal error # 3&4xx not generated at top-level # 5xx = error but events saved for later processing - if msgnum // 100 == 2: - logger.warning(f'EDSM\t{msgnum} {msg}\t{json.dumps(pending, separators = (",", ": "))}') + if msg_num // 100 == 2: + logger.warning(f'EDSM\t{msg_num} {msg}\t{json.dumps(pending, separators=(",", ": "))}') plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg)) else: From 25adf297793fdacb8e2294f46a1b3a679702f2b5 Mon Sep 17 00:00:00 2001 From: A_D Date: Wed, 19 Aug 2020 12:27:15 +0200 Subject: [PATCH 36/71] Fixed possible error if credentials is None --- plugins/edsm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 766c2dd3..3b12c5ed 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -491,7 +491,11 @@ def worker() -> None: if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): logger.debug('CarrierJump (or FSDJump) in pending and it passed should_send()') - (username, apikey) = credentials(cmdr) # TODO: This raises if credentials returns None + creds = credentials(cmdr) # TODO: possibly unbound + if creds is None: + raise ValueError("Unexpected lack of credentials") + + (username, apikey) = creds data = { 'commanderName': username.encode('utf-8'), 'apiKey': apikey, From 7ab697a80757fa00c9d0612fbb24da7f6a74bd23 Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 25 Aug 2020 11:56:14 +0200 Subject: [PATCH 37/71] Used default option for .get on dicts --- plugins/edsm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 3b12c5ed..15999fca 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -319,8 +319,8 @@ entry: {entry!r}''' # Always update our system address even if we're not currently the provider for system or station, but dont update # on events that contain "future" data, such as FSDTarget if entry['event'] in ('Location', 'Docked', 'CarrierJump', 'FSDJump'): - this.system_address = entry.get('SystemAddress') or this.system_address - this.system = entry.get('StarSystem') or this.system + this.system_address = entry.get('SystemAddress', this.system_address) + this.system = entry.get('StarSystem', this.system) # We need pop == 0 to set the value so as to clear 'x' in systems with # no stations. @@ -328,8 +328,8 @@ entry: {entry!r}''' if pop is not None: this.system_population = pop - this.station = entry.get('StationName') or this.station - this.station_marketid = entry.get('MarketID') or this.station_marketid + this.station = entry.get('StationName', this.station) + this.station_marketid = entry.get('MarketID', this.station) # We might pick up StationName in DockingRequested, make sure we clear it if leaving if entry['event'] in ('Undocked', 'FSDJump', 'SupercruiseEntry'): this.station = None From 62f3203c3fabbab5729d6548c588e12001d94c30 Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 25 Aug 2020 12:00:30 +0200 Subject: [PATCH 38/71] Added type checker hint for unbound names Due to the fact that cmdr and entry are only assigned if item exists, a situation can arise where any access to the names will raise an UnboundLocalException, this tells the type checker to ignore that possibility by using a TYPE_CHECKING guarded assignment to those names. This does not fix the issue at runtime, it just tells the type checker that its fine. As this remains a bug, I have left TODOs in to note its existence. --- plugins/edsm.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 15999fca..f13517a4 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -468,13 +468,14 @@ def worker() -> None: retrying = 0 while retrying < 3: try: - # TODO: Technically entry can be unbound here. - if item and entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'): - logger.debug(f'{entry["event"]}') + if TYPE_CHECKING: + # Tell the type checker that these two are bound. + # TODO: While this works because of the item check below, these names are still technically unbound + # TODO: in some cases, therefore this should be refactored. + cmdr: str = "" + entry: Mapping[str, Any] = {} - if item and entry['event'] not in this.discardedEvents: - if entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'): - logger.debug(f'{entry["event"]} event not in discarded list') + if item and entry['event'] not in this.discardedEvents: # TODO: Technically entry can be unbound here. pending.append(entry) # Get list of events to discard From f2add920c702b83ca377be8d187a456fd6d99a8f Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 25 Aug 2020 12:06:26 +0200 Subject: [PATCH 39/71] replaced constant contains checks with tuples --- plugins/edsm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index f13517a4..9868d57f 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -361,7 +361,7 @@ entry: {entry!r}''' elif entry['event'] == 'LoadGame': this.coordinates = None - if entry['event'] in ['LoadGame', 'Commander', 'NewCommander']: + if entry['event'] in ('LoadGame', 'Commander', 'NewCommander'): this.newgame = True this.newgame_docked = False this.navbeaconscan = 0 @@ -523,7 +523,7 @@ def worker() -> None: else: for e, r in zip(pending, reply['events']): - if not closing and e['event'] in ['StartUp', 'Location', 'FSDJump', 'CarrierJump']: + if not closing and e['event'] in ('StartUp', 'Location', 'FSDJump', 'CarrierJump'): # Update main window's system status this.lastlookup = r # calls update_status in main thread @@ -558,7 +558,7 @@ def should_send(entries: List[Mapping[str, Any]]) -> bool: else: assert(False) - this.navbeaconscan = 0 + this.navbeaconscan = 0 # TODO: Unreachable code? for entry in entries: if (entry['event'] == 'Cargo' and not this.newgame_docked) or entry['event'] == 'Docked': @@ -570,10 +570,10 @@ def should_send(entries: List[Mapping[str, Any]]) -> bool: elif this.newgame: pass - elif entry['event'] not in [ + elif entry['event'] not in ( 'CommunityGoal', # Spammed periodically 'ModuleBuy', 'ModuleSell', 'ModuleSwap', # will be shortly followed by "Loadout" - 'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap']: # " + 'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap'): # " return True return False From 893cab3c23331ba89eb6365c8f0c16d23a63c869 Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 25 Aug 2020 12:11:14 +0200 Subject: [PATCH 40/71] cleaned up logic where possible --- plugins/edsm.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 9868d57f..2f3baa1f 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -216,7 +216,7 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: - this.log_button['state'] = cmdr and not is_beta and tk.NORMAL or tk.DISABLED + this.log_button['state'] = tk.NORMAL if cmdr and not is_beta else tk.DISABLED this.user['state'] = tk.NORMAL this.user.delete(0, tk.END) this.apikey['state'] = tk.NORMAL @@ -350,7 +350,7 @@ entry: {entry!r}''' # Update display of 'EDSM Status' image if this.system_link['text'] != system: - this.system_link['text'] = system or '' + this.system_link['text'] = system if system else '' this.system_link['image'] = '' this.system_link.update_idletasks() @@ -418,12 +418,16 @@ def cmdr_data(data: Mapping[str, Any], is_beta: bool) -> None: system = data['lastSystem']['name'] # Always store initially, even if we're not the *current* system provider. - if not this.station_marketid: - this.station_marketid = data['commander']['docked'] and data['lastStarport']['id'] + if not this.station_marketid and data['commander']['docked']: + this.station_marketid = data['lastStarport']['id'] # Only trust CAPI if these aren't yet set - this.system = this.system or data['lastSystem']['name'] - this.station = this.station or data['commander']['docked'] and data['lastStarport']['name'] + if not this.system: + this.system = data['lastSystem']['name'] + + if not this.station and data['commander']['docked']: + this.station = data['lastStarport']['name'] + # TODO: Fire off the EDSM API call to trigger the callback for the icons if config.get('system_provider') == 'EDSM': @@ -486,7 +490,7 @@ def worker() -> None: this.discardedEvents.discard('Docked') # should_send() assumes that we send 'Docked' events assert this.discardedEvents # wouldn't expect this to be empty # Filter out unwanted events - pending = [x for x in pending if x['event'] not in this.discardedEvents] + pending = list(filter(lambda x: x['event'] not in this.discardedEvents, pending)) if should_send(pending): if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): From fb080f80807a6a3870f0ab311ae4c3cc80958fac Mon Sep 17 00:00:00 2001 From: A_D Date: Mon, 7 Sep 2020 00:39:39 +0200 Subject: [PATCH 41/71] Removed unused imports --- plugins/edsm.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 2f3baa1f..edf5f497 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -15,9 +15,6 @@ import json import logging import sys import tkinter as tk -import urllib.error -import urllib.parse -import urllib.request from queue import Queue from threading import Thread from typing import TYPE_CHECKING, Any, List, Mapping, MutableMapping, Optional, Tuple From 26be93f92e5220031d88214471a2c4a9307501dd Mon Sep 17 00:00:00 2001 From: A_D Date: Mon, 7 Sep 2020 00:43:43 +0200 Subject: [PATCH 42/71] replaced magic number row with var --- plugins/edsm.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index edf5f497..691ad27a 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -107,6 +107,7 @@ def station_url(system_name: str, station_name: str) -> str: return '' + def plugin_start3(plugin_dir: str) -> str: # Can't be earlier since can only call PhotoImage after window is created this._IMG_KNOWN = tk.PhotoImage(data=IMG_KNOWN_B64) # green circle @@ -190,22 +191,28 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: underline=True ) + cur_row = 10 + this.label.grid(columnspan=2, padx=PADX, sticky=tk.W) this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window - this.cmdr_label.grid(row=10, padx=PADX, sticky=tk.W) + this.cmdr_label.grid(row=cur_row, padx=PADX, sticky=tk.W) this.cmdr_text = nb.Label(frame) - this.cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W) + this.cmdr_text.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.W) + + cur_row += 1 this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting - this.user_label.grid(row=11, padx=PADX, sticky=tk.W) + this.user_label.grid(row=cur_row, padx=PADX, sticky=tk.W) this.user = nb.Entry(frame) - this.user.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) + this.user.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.EW) + + cur_row += 1 this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting - this.apikey_label.grid(row=12, padx=PADX, sticky=tk.W) + this.apikey_label.grid(row=cur_row, padx=PADX, sticky=tk.W) this.apikey = nb.Entry(frame) - this.apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW) + this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.EW) prefs_cmdr_changed(cmdr, is_beta) From 4a7cddc48bf9dba692f5c841aedde7283db42b5a Mon Sep 17 00:00:00 2001 From: A_D Date: Thu, 17 Sep 2020 09:15:12 +0200 Subject: [PATCH 43/71] removed assert False --- plugins/edsm.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 691ad27a..4a62b5d3 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -565,8 +565,11 @@ def should_send(entries: List[Mapping[str, Any]]) -> bool: return False else: - assert(False) - this.navbeaconscan = 0 # TODO: Unreachable code? + logger.error( + 'Invalid state NavBeaconScan exists, but passed entries either ' + "doesn't exist or doesn't have the expected content" + ) + this.navbeaconscan = 0 for entry in entries: if (entry['event'] == 'Cargo' and not this.newgame_docked) or entry['event'] == 'Docked': From 615a36452d855a37109fb3c94ae69e47f403b4fe Mon Sep 17 00:00:00 2001 From: A_D Date: Thu, 17 Sep 2020 09:26:22 +0200 Subject: [PATCH 44/71] Added docstrings --- plugins/edsm.py | 53 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 4a62b5d3..e33a9885 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -1,6 +1,4 @@ -# -# System display and EDSM lookup -# +"""System display and EDSM lookup.""" # TODO: # 1) Re-factor EDSM API calls out of journal_entry() into own function. @@ -85,6 +83,7 @@ plEAADs= # Main window clicks def system_url(system_name: str) -> str: + """Get a URL for the current system.""" if this.system_address: return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemID64={this.system_address}') @@ -95,20 +94,28 @@ def system_url(system_name: str) -> str: def station_url(system_name: str, station_name: str) -> str: + """Get a URL for the current station.""" if system_name and station_name: - return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemName={system_name}&stationName={station_name}') + return requests.utils.requote_uri( + f'https://www.edsm.net/en/system?systemName={system_name}&stationName={station_name}' + ) # monitor state might think these are gone, but we don't yet if this.system and this.station: - return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemName={this.system}&stationName={this.station}') + return requests.utils.requote_uri( + f'https://www.edsm.net/en/system?systemName={this.system}&stationName={this.station}' + ) if system_name: - return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemName={system_name}&stationName=ALL') + return requests.utils.requote_uri( + f'https://www.edsm.net/en/system?systemName={system_name}&stationName=ALL' + ) return '' def plugin_start3(plugin_dir: str) -> str: + """Plugin setup hook.""" # 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 @@ -144,21 +151,24 @@ def plugin_start3(plugin_dir: str) -> str: def plugin_app(parent: tk.Tk) -> None: + """Plugin UI setup.""" this.system_link = parent.children['system'] # system label in main window this.system_link.bind_all('<>', update_status) this.station_link = parent.children['station'] # station label in main window def plugin_stop() -> None: + """Plugin exit hook.""" # Signal thread to close and wait for it this.queue.put(None) this.thread.join() this.thread = None - # Suppress 'Exception ignored in: ' errors + # Suppress 'Exception ignored in: ' errors # TODO: this is bad. this._IMG_KNOWN = this._IMG_UNKNOWN = this._IMG_NEW = this._IMG_ERROR = None def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame: + """Plugin preferences setup hook.""" PADX = 10 # noqa: N806 BUTTONX = 12 # indent Checkbuttons and Radiobuttons # noqa: N806 PADY = 2 # close spacing # noqa: N806 @@ -220,6 +230,7 @@ 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.""" 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) @@ -244,6 +255,7 @@ def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None: def prefsvarchanged() -> None: + """Preferences screen closed hook.""" to_set = tk.DISABLED if this.log.get(): to_set = this.log_button['state'] @@ -253,7 +265,7 @@ def prefsvarchanged() -> None: def set_prefs_ui_states(state: str) -> None: """ - Set the state of various config UI entries + Set the state of various config UI entries. :param state: the state to set each entry to """ @@ -267,6 +279,7 @@ def set_prefs_ui_states(state: str) -> None: def prefs_changed(cmdr: str, is_beta: bool) -> None: + """Preferences changed hook.""" config.set('edsm_out', this.log.get()) if cmdr and not is_beta: @@ -291,6 +304,12 @@ def prefs_changed(cmdr: str, is_beta: bool) -> None: def credentials(cmdr: str) -> Optional[Tuple[str, str]]: + """ + Get credentials for the given commander, if they exist. + + :param cmdr: The commander to get credentials for + :return: The credentials, or None + """ # Credentials for cmdr if not cmdr: return None @@ -312,6 +331,7 @@ def credentials(cmdr: str) -> Optional[Tuple[str, str]]: def journal_entry( cmdr: str, is_beta: bool, system: str, station: str, entry: MutableMapping[str, Any], state: Mapping[str, Any] ) -> None: + """Journal Entry hook.""" if entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'): logger.debug(f'''{entry["event"]} Commander: {cmdr} @@ -419,6 +439,7 @@ Queueing: {entry!r}''' # Update system data def cmdr_data(data: Mapping[str, Any], is_beta: bool) -> None: + """CAPI Entry Hook.""" system = data['lastSystem']['name'] # Always store initially, even if we're not the *current* system provider. @@ -463,6 +484,11 @@ def cmdr_data(data: Mapping[str, Any], is_beta: bool) -> None: # Worker thread def worker() -> None: + """ + Upload worker. + + Works based the `this.queue` queue, loops infinitely until the item returned from the queue is None. + """ pending = [] # Unsent events closing = False @@ -555,8 +581,13 @@ def worker() -> None: return -# Whether any of the entries should be sent immediately def should_send(entries: List[Mapping[str, Any]]) -> bool: + """ + Whether or not any of the given entries should be sent immediately. + + :param entries: The entries to check + :return: bool indicating whether or not to send said entries + """ # batch up burst of Scan events after NavBeaconScan if this.navbeaconscan: if entries and entries[-1]['event'] == 'Scan': @@ -590,9 +621,8 @@ def should_send(entries: List[Mapping[str, Any]]) -> bool: return False -# Call edsm_notify_system() in this and other interested plugins with EDSM's response to a 'StartUp', 'Location', -# 'FSDJump' or 'CarrierJump' event def update_status(event=None) -> None: + """Update listening plugins with our response to StartUp, Location, FSDJump, or CarrierJump.""" for plugin in plug.provides('edsm_notify_system'): plug.invoke(plugin, None, 'edsm_notify_system', this.lastlookup) @@ -601,6 +631,7 @@ def update_status(event=None) -> None: # https://www.edsm.net/en/api-journal-v1 # msgnum: 1xx = OK, 2xx = fatal error, 3xx = error, 4xx = ignorable errors. def edsm_notify_system(reply: Mapping[str, Any]) -> None: + """Update the image next to the system link.""" if not reply: this.system_link['image'] = this._IMG_ERROR plug.show_error(_("Error: Can't connect to EDSM")) From 375573c0a8c24b37e7b0b57cbb608aa5a6aae159 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 23:17:36 +0200 Subject: [PATCH 45/71] fixed docstrings --- plugins/edsm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index e33a9885..8a2e09c4 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -487,7 +487,7 @@ def worker() -> None: """ Upload worker. - Works based the `this.queue` queue, loops infinitely until the item returned from the queue is None. + Processes `this.queue` until the queued item is None. """ pending = [] # Unsent events closing = False @@ -583,7 +583,7 @@ def worker() -> None: def should_send(entries: List[Mapping[str, Any]]) -> bool: """ - Whether or not any of the given entries should be sent immediately. + Whether or not any of the given entries should be sent to EDSM. :param entries: The entries to check :return: bool indicating whether or not to send said entries From 9710a5e9bbc7dd6b6d5f3a50b81f143c30604c30 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 23:18:10 +0200 Subject: [PATCH 46/71] repalced assert with log and continue --- plugins/edsm.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 8a2e09c4..d84696d1 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -518,7 +518,13 @@ def worker() -> None: r.raise_for_status() this.discardedEvents = set(r.json()) this.discardedEvents.discard('Docked') # should_send() assumes that we send 'Docked' events - assert this.discardedEvents # wouldn't expect this to be empty + if not this.discardedEvents: + logger.error( + 'Unexpected empty discarded events list from EDSM. Bailing out of send: ' + f'{type(this.discardedEvents)} -- {this.discardedEvents}' + ) + continue + # Filter out unwanted events pending = list(filter(lambda x: x['event'] not in this.discardedEvents, pending)) From e93091141521db4fd760210fa7066b08ff567f89 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 23:18:29 +0200 Subject: [PATCH 47/71] remove double assign --- plugins/edsm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index d84696d1..9c3137e9 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -552,7 +552,8 @@ def worker() -> None: r = this.session.post('https://www.edsm.net/api-journal-v1', data=data, timeout=_TIMEOUT) r.raise_for_status() reply = r.json() - (msg_num, msg) = reply['msgnum'], reply['msg'] + msg_num = reply['msgnum'] + msg = reply['msg'] # 1xx = OK # 2xx = fatal error # 3&4xx not generated at top-level From f8523e0a9f1e4b57c57fcc0cf540223eee01a966 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 23:19:44 +0200 Subject: [PATCH 48/71] removed parens around tuple unpack --- plugins/edsm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 9c3137e9..74127fa5 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -536,7 +536,7 @@ def worker() -> None: if creds is None: raise ValueError("Unexpected lack of credentials") - (username, apikey) = creds + username, apikey = creds data = { 'commanderName': username.encode('utf-8'), 'apiKey': apikey, From c02b292f50d9009ddbef0cc8117e2c190eece20d Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 22 Sep 2020 14:51:45 +0200 Subject: [PATCH 49/71] Removed uneeded list comps --- plugins/edsm.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 74127fa5..2bc42b4c 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -339,7 +339,7 @@ System: {system} Station: {station} state: {state!r} entry: {entry!r}''' - ) + ) # Always update our system address even if we're not currently the provider for system or station, but dont update # on events that contain "future" data, such as FSDTarget if entry['event'] in ('Location', 'Docked', 'CarrierJump', 'FSDJump'): @@ -529,7 +529,7 @@ def worker() -> None: pending = list(filter(lambda x: x['event'] not in this.discardedEvents, pending)) if should_send(pending): - if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): + if any(p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')): logger.debug('CarrierJump (or FSDJump) in pending and it passed should_send()') creds = credentials(cmdr) # TODO: possibly unbound @@ -545,10 +545,11 @@ def worker() -> None: 'message': json.dumps(pending, ensure_ascii=False).encode('utf-8'), } - if any([p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')]): + if any(p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')): data_elided = data.copy() data_elided['apiKey'] = '' logger.debug(f'CarrierJump (or FSDJump): Attempting API call\ndata: {data_elided!r}') + r = this.session.post('https://www.edsm.net/api-journal-v1', data=data, timeout=_TIMEOUT) r.raise_for_status() reply = r.json() From 4ecb378b8345dfcf8318a58389ff428e75412b23 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 08:03:07 +0200 Subject: [PATCH 50/71] cleaned up plugins.md --- PLUGINS.md | 173 ++++++++++++++++++++++++++++------------------------- 1 file changed, 93 insertions(+), 80 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 3eba7d06..45bc8842 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -3,6 +3,7 @@ Plugins allow you to customise and extend the behavior of EDMC. ## Installing a Plugin + See [Plugins](https://github.com/EDCD/EDMarketConnector/wiki/Plugins) on the wiki. @@ -12,16 +13,16 @@ Plugins are loaded when EDMC starts up. Each plugin has it's own folder in the `plugins` directory: -* Windows: `%LOCALAPPDATA%\EDMarketConnector\plugins` -* Mac: `~/Library/Application Support/EDMarketConnector/plugins` -* Linux: `$XDG_DATA_HOME/EDMarketConnector/plugins`, or - `~/.local/share/EDMarketConnector/plugins` if `$XDG_DATA_HOME` is unset. +- Windows: `%LOCALAPPDATA%\EDMarketConnector\plugins` +- Mac: `~/Library/Application Support/EDMarketConnector/plugins` +- Linux: `$XDG_DATA_HOME/EDMarketConnector/plugins`, or `~/.local/share/EDMarketConnector/plugins` if `$XDG_DATA_HOME` is unset. Plugins are python files. The plugin folder must have a file named `load.py` that must provide one module level function and optionally provide a few others. --- + ### Available imports **`import`ing anything from the core EDMarketConnector code that is not @@ -32,34 +33,36 @@ breaking with future code changes.** `from theme import theme` - So plugins can theme their own UI elements to match the main UI. - + `from config import appname, applongname, appcmdname, appversion , copyright, config` - to access config. `from prefs import prefsVersion` - to allow for versioned preferences. `from companion import category_map` - Or any of the other static date - contained therein. NB: There's a plan to move such to a `data` module. +contained therein. NB: There's a plan to move such to a `data` module. `import plug` - Mostly for using `plug.show_error()`. Also the flags - for `dashboard_entry()` to be useful (see example below). Relying on anything - else isn't supported. - +for `dashboard_entry()` to be useful (see example below). Relying on anything +else isn't supported. + `from monitor import gamerunning` - in case a plugin needs to know if we think the game is running. `import timeout_session` - provides a method called `new_session` that creates a requests.session with a default timeout on all requests. Recommended to reduce noise in HTTP requests - ```python from ttkHyperlinkLabel import HyperlinkLabel import myNotebook as nb ``` + For creating UI elements. --- + ### Logging + In the past the only way to provide any logged output from a plugin was to use `print(...)` statements. When running the application from the packaged executeable all output is redirected to a log file. See @@ -72,6 +75,7 @@ statements. Insert this at the top-level of your load.py file (so not inside `plugin_start3()` ): + ```python import logging @@ -149,7 +153,9 @@ functions, in the output. ``` --- + ### Startup + EDMC will import the `load.py` file as a module and then call the `plugin_start3()` function. @@ -161,6 +167,7 @@ def plugin_start3(plugin_dir): print("I am loaded! My plugin folder is {}".format(plugin_dir)) return "Test" ``` + The string you return is used as the internal name of the plugin. Any errors or print statements from your plugin will appear in @@ -168,6 +175,7 @@ Any errors or print statements from your plugin will appear in Mac, and `$TMP/EDMarketConnector.log` on Linux. ### Shutdown + This gets called when the user closes the program: ```python @@ -182,7 +190,8 @@ If your plugin uses one or more threads to handle Events then stop and join() the threads before returning from this function. ## Plugin Hooks -### Configuration + +### Configuration If you want your plugin to be configurable via the GUI you can define a frame (panel) to be displayed on its own tab in EDMC's settings dialog. The tab @@ -204,7 +213,7 @@ import tkinter as tk import myNotebook as nb from config import config -this = sys.modules[__name__] # For holding module globals +this = sys.modules[__name__] # For holding module globals def plugin_prefs(parent, cmdr, is_beta): """ @@ -242,7 +251,7 @@ You can use `stringFromNumber()` from EDMC's `l10n.Locale` object to format numbers in your widgets in a locale-independent way. ```python -this = sys.modules[__name__] # For holding module globals +this = sys.modules[__name__] # For holding module globals def plugin_app(parent): """ @@ -251,7 +260,7 @@ def plugin_app(parent): label = tk.Label(parent, text="Status:") # By default widgets inherit the current theme's colors this.status = tk.Label(parent, text="", foreground="yellow") # Override theme's foreground color return (label, this.status) - + # later on your event functions can update the contents of these widgets this.status["text"] = "Happy!" this.status["foreground"] = "green" @@ -314,7 +323,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): ``` This gets called when EDMC sees a new entry in the game's journal. - + - `cmdr` is a `str` denoting the current Commander Name. - `is_beta` is a `bool` denoting if data came from a beta version of the game. - `system` is a `str` holding the name of the current system, or `None` if not @@ -324,38 +333,37 @@ This gets called when EDMC sees a new entry in the game's journal. - `entry` is an `OrderedDict` holding the Journal event. - `state` is a `dictionary` containing information about the Cmdr and their ship and cargo (including the effect of the current journal entry). - - `Captain` - `str` of name of Commander's crew you joined in multi-crew, - else `None` - - `Cargo` - `dict` with details of current cargo. - - `Credits` - Current credit balance. - - `FID` - Frontier Cmdr ID - - `Horizons` - `bool` denoting if Horizons expansion active. - - `Loan` - Current loan amount, else None. - - `Raw` - `dict` with details of "Raw" materials held. - - `Manufactured` - `dict` with details of "Manufactured" materials held. - - `Encoded` - `dict` with details of "Encoded" materials held. - - `Engineers` - `dict` with details of Rank Progress for Engineers. - - `Rank` - `dict` of current Ranks. Each entry is a `tuple` of - (, ) - - `Reputation` - `dict` of Major Faction reputations, scale is -100 to +100 - See Frontier's Journal Manual for detail of bands. - - `Statistics` - `dict` of a Journal "Statistics" event, i.e. data shown - in the statistics panel on the right side of the cockpit. See Frontier's - Journal Manual for details. - - `Role` - Crew role if multi-crewing in another Commander's ship: - - `None` - - "Idle" - - "FireCon" - - "FighterCon" - - `Friends` -`set` of online friends. - - `ShipID` - `int` that denotes Frontier internal ID for your current ship. - - `ShipIdent` - `str` of your current ship's textual ID (which you set). - - `ShipName` - `str` of your current ship's textual Name (which you set). - - `ShipType` - `str` of your current ship's model, e.g. "CobraMkIII". - - `HullValue` - `int` of current ship's credits value, excluding modules. - - `ModulesValue` - `int` of current ship's module's total credits value. - - `Rebuy` - `int` of current ship's rebuy cost in credits. - - `Modules` - `dict` with data on currently fitted modules. + - `Captain` - `str` of name of Commander's crew you joined in multi-crew, + else `None` + - `Cargo` - `dict` with details of current cargo. + - `Credits` - Current credit balance. + - `FID` - Frontier Cmdr ID + - `Horizons` - `bool` denoting if Horizons expansion active. + - `Loan` - Current loan amount, else None. + - `Raw` - `dict` with details of "Raw" materials held. + - `Manufactured` - `dict` with details of "Manufactured" materials held. + - `Encoded` - `dict` with details of "Encoded" materials held. + - `Engineers` - `dict` with details of Rank Progress for Engineers. + - `Rank` - `dict` of current Ranks. Each entry is a 2 `tuple` of ints, rank and age + - `Reputation` - `dict` of Major Faction reputations, scale is -100 to +100 + See Frontier's Journal Manual for detail of bands. + - `Statistics` - `dict` of a Journal "Statistics" event, i.e. data shown + in the statistics panel on the right side of the cockpit. See Frontier's + Journal Manual for details. + - `Role` - Crew role if multi-crewing in another Commander's ship: + - `None` + - "Idle" + - "FireCon" + - "FighterCon" + - `Friends` -`set` of online friends. + - `ShipID` - `int` that denotes Frontier internal ID for your current ship. + - `ShipIdent` - `str` of your current ship's textual ID (which you set). + - `ShipName` - `str` of your current ship's textual Name (which you set). + - `ShipType` - `str` of your current ship's model, e.g. "CobraMkIII". + - `HullValue` - `int` of current ship's credits value, excluding modules. + - `ModulesValue` - `int` of current ship's module's total credits value. + - `Rebuy` - `int` of current ship's rebuy cost in credits. + - `Modules` - `dict` with data on currently fitted modules. A special "StartUp" entry is sent if EDMC is started while the game is already running. In this case you won't receive initial events such as "LoadGame", @@ -366,7 +374,6 @@ Similarly, a special "ShutDown" entry is sent when the game is quitted while EDMC is running. This event is not sent when EDMC is running on a different machine so you should not *rely* on receiving this event. - #### Player Dashboard ```python @@ -379,16 +386,16 @@ def dashboard_entry(cmdr, is_beta, entry): This gets called when something on the player's cockpit display changes - typically about once a second when in orbital flight. - - `cmdr` is a `str` denoting the current Commander Name. - `is_beta` is a `bool` denoting if data came from a beta version of the game. - `entry` is a `dict` loaded from the Status.json file the game writes. - See the "Status File" section in the Frontier [Journal documentation](https://forums.frontier.co.uk/showthread.php/401661) - for the available `entry` properties and for the list of available `"Flags"`. - Ask on the EDCD Discord server to be sure you have the latest version. - Refer to the source code of [plug.py](./plug.py) for the list of available - constants. + See the "Status File" section in the Frontier [Journal documentation](https://forums.frontier.co.uk/showthread.php/401661) + for the available `entry` properties and for the list of available `"Flags"`. + Ask on the EDCD Discord server to be sure you have the latest version. + Refer to the source code of [plug.py](./plug.py) for the list of available + constants. + #### Getting Commander Data ```python @@ -404,10 +411,10 @@ Frontier's servers. - `data` is a dictionary containing the response from Frontier to a CAPI `/profile` request, augmented with two extra keys: - - `marketdata` - contains the CAPI data from the `/market` endpoint, if - docked and the station has the commodites service. - - `shipdata` - contains the CAPI data from the `/shipyard` endpoint, if - docked and the station has the shipyard service. + - `marketdata` - contains the CAPI data from the `/market` endpoint, if + docked and the station has the commodities service. + - `shipdata` - contains the CAPI data from the `/shipyard` endpoint, if + docked and the station has the shipyard service. - `is_beta` is a `bool` denoting if data came from a beta version of the game. #### Plugin-specific events @@ -432,21 +439,24 @@ called when the player starts the game or enters a new system. It is called some time after the corresponding `journal_entry()` event. --- + ```python def inara_notify_location(eventData): """ `eventData` holds the response to one of the "Commander's Flight Log" events https://inara.cz/inara-api-docs/#event-29 """ if eventData.get('starsystemInaraID'): - sys.stderr.write('Now in Inara system {ID} at {URL}\n'.format(ID=eventData['starsystemInaraID'], - URL=eventData['starsystemInaraURL']) - ) + sys.stderr.write('Now in Inara system {ID} at {URL}\n'.format( + ID=eventData['starsystemInaraID'], + URL=eventData['starsystemInaraURL'] + )) else: sys.stderr.write('System not known to Inara\n') if eventData.get('stationInaraID'): - sys.stderr.write('Docked at Inara station {ID} at {URL}\n'.format(ID=eventData['stationInaraID'], - URL=eventData['stationInaraURL']) - ) + sys.stderr.write('Docked at Inara station {ID} at {URL}\n'.format( + ID=eventData['stationInaraID'], + URL=eventData['stationInaraURL'] + )) else: sys.stderr.write('Undocked or station unknown to Inara\n') ``` @@ -457,15 +467,17 @@ undocks. It is called some time after the corresponding `journal_entry()` event. --- + ```python def inara_notify_ship(eventData): """ `eventData` holds the response to an addCommanderShip or setCommanderShip event https://inara.cz/inara-api-docs/#event-11 """ if eventData.get('shipInaraID'): - sys.stderr.write('Now in Inara ship {ID} at {URL}\n'.format(ID=eventData['shipInaraID'], - URL=eventData['shipInaraURL']) - ) + sys.stderr.write('Now in Inara ship {ID} at {URL}\n'.format( + ID=eventData['shipInaraID'], + URL=eventData['shipInaraURL'] + )) ``` If the player has chosen to "Send flight log and Cmdr status to Inara" this @@ -517,7 +529,6 @@ See EDMC's own [`L10n`](https://github.com/EDCD/EDMarketConnector/tree/master/L1 folder for the list of supported language codes and for example translation files. - ## Python Package Plugins A _Package Plugin_ is both a standard Python package (i.e. contains an @@ -528,15 +539,14 @@ before any non-Package plugins. Other plugins can access features in a Package Plugin by `import`ing the package by name in the usual way. - ## Distributing a Plugin To package your plugin for distribution simply create a `.zip` archive of your plugin's folder: -* Windows: In Explorer right click on your plugin's folder and choose Send to - → Compressed (zipped) folder. -* Mac: In Finder right click on your plugin's folder and choose Compress. +- Windows: In Explorer right click on your plugin's folder and choose Send to + → Compressed (zipped) folder. +- Mac: In Finder right click on your plugin's folder and choose Compress. If there are any external dependencies then include them in the plugin's folder. @@ -558,19 +568,22 @@ release under Python 3.7 will be 4.0.0.0. This is a brief outline of the steps required to migrate a plugin from earlier versions of EDMC: - Rename the function `plugin_start` to `plugin_start3(plugin_dir)`. - Plugins without a `plugin_start3` function are listed as disabled on EDMC's - "Plugins" tab and a message like "plugin SuperSpaceHelper needs migrating" - appears in the log. Such plugins are also listed in a section "Plugins Without - Python 3.x Support:" on the Settings > Plugins tab. + Plugins without a `plugin_start3` function are listed as disabled on EDMC's + "Plugins" tab and a message like "plugin SuperSpaceHelper needs migrating" + appears in the log. Such plugins are also listed in a section "Plugins Without + Python 3.x Support:" on the Settings > Plugins tab. + - Check that callback functions `plugin_prefs`, `prefs_changed`, - `journal_entry`, `dashboard_entry` and `cmdr_data` if used are declared with - the correct number of arguments. Older versions of this app were tolerant - of missing arguments in these function declarations. + `journal_entry`, `dashboard_entry` and `cmdr_data` if used are declared with + the correct number of arguments. Older versions of this app were tolerant + of missing arguments in these function declarations. + - Port the code to Python 3.7. The [2to3](https://docs.python.org/3/library/2to3.html) - tool can automate much of this work. + tool can automate much of this work. Depending on the complexity of the plugin it may be feasible to make it compatible with both EDMC 3.4 + Python 2.7 and EDMC 3.5 + Python 3.7. + [Here's](https://python-future.org/compatible_idioms.html) a guide on writing Python 2/3 compatible code and [here's](https://github.com/Marginal/HabZone/commit/3c41cd41d5ad81ef36aab40e967e3baf77b4bd06) an example of the changes required for a simple plugin. From 470cbaa146b6d6a4ae683e07ab9d11e237c36024 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 12:02:29 +0200 Subject: [PATCH 51/71] Added example plugin --- docs/examples/click_counter/load.py | 126 ++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/examples/click_counter/load.py diff --git a/docs/examples/click_counter/load.py b/docs/examples/click_counter/load.py new file mode 100644 index 00000000..7a4c9f49 --- /dev/null +++ b/docs/examples/click_counter/load.py @@ -0,0 +1,126 @@ +""" +Example EDMC plugin. + +It adds a single button to the EDMC interface that displays the number of times it has been reset. +""" + +import logging +import tkinter as tk +from typing import Optional + +import myNotebook as nb +from config import appname, config + +PLUGIN_NAME = "ClickCounter" + +logger = logging.getLogger(f"{appname}") + + +class ClickCounter: + """ + ClickCounter implements the EDMC plugin interface. + + It adds a button to the EDMC UI that displays the number of times it has been clicked, and a preference to set + the number directly. + """ + + def __init__(self) -> None: + # Be sure to use names that wont collide in our config variables + self.click_count: Optional[tk.StringVar] = tk.StringVar(value=str(config.getint('click_counter_count'))) + logger.info("ClickCounter instantiated") + + def on_load(self) -> str: + """ + on_load is called by plugin_start3 below. + + It is the first point EDMC interacts with our code after loading our module. + + :return: The name of the plugin, which will be used by EDMC for logging and for the settings window + """ + return PLUGIN_NAME + + def on_unload(self) -> None: + """ + on_unload is called by plugin_stop below. + + It is the last thing called before EDMC shuts down. Note that blocking code here will hold the shutdown process. + """ + self.on_preferences_closed("", False) # Save our prefs + + def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: + """ + setup_preferences is called by plugin_prefs below. + + It is where we can setup our own settings page in EDMC's settings window. Our tab is defined for us. + + :param parent: the tkinter parent that our returned Frame will want to inherit from + :param cmdr: The current ED Commander + :param is_beta: Whether or not EDMC is currently marked as in beta mode + :return: The frame to add to the settings window + """ + current_row = 0 + frame = nb.Frame(parent) + + # setup our config in a "Click Count: number" + nb.Label(frame, text='Click Count').grid(row=current_row) + nb.Entry(frame, textvariable=self.click_count).grid(row=current_row, column=1) + current_row += 1 # Always increment our row counter, makes for far easier tkinter design. + return frame + + def on_preferences_closed(self, cmdr: str, is_beta: bool) -> None: + """ + on_preferences_closed is called by prefs_changed below. + + It is called when the preferences dialog is dismissed by the user. + + :param cmdr: The current ED Commander + :param is_beta: Whether or not EDMC is currently marked as in beta mode + """ + config.set('click_counter_count', self.click_count.get()) + + def setup_main_ui(self, parent: tk.Frame) -> tk.Frame: + """ + Create our entry on the main EDMC UI. + + This is called by plugin_app below. + + :param parent: EDMC main window Tk + :return: Our frame + """ + current_row = 0 + frame = tk.Frame(parent) + button = tk.Button( + frame, + text="Count me", + command=lambda: self.click_count.set(str(int(self.click_count.get()) + 1)) + ) + button.grid(row=current_row) + current_row += 1 + nb.Label(frame, text="Count:").grid(row=current_row, sticky=tk.W) + nb.Label(frame, textvariable=self.click_count).grid(row=current_row, column=1) + return frame + + +cc = ClickCounter() + + +# Note that all of these could be simply replaced with something like: +# plugin_start3 = cc.on_load +def plugin_start3(plugin_dir: str) -> str: + return cc.on_load() + + +def plugin_stop() -> None: + return cc.on_unload() + + +def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: + return cc.setup_preferences(parent, cmdr, is_beta) + + +def prefs_changed(cmdr: str, is_beta: bool) -> None: + return cc.on_preferences_closed(cmdr, is_beta) + + +def plugin_app(parent: tk.Frame) -> Optional[tk.Frame]: + return cc.setup_main_ui(parent) From 161ca24bb6bb706d82fd07c6426a98a16355570b Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 12:02:39 +0200 Subject: [PATCH 52/71] updated plugin docs --- PLUGINS.md | 59 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 45bc8842..d86053df 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -23,6 +23,12 @@ others. --- +### Examples + +We have some example plugins available in the docs/examples directory. See the readme in each folder for more info. + +--- + ### Available imports **`import`ing anything from the core EDMarketConnector code that is not @@ -73,8 +79,7 @@ EDMC now implements proper logging using the Python `logging` module. Plugin developers should now use the following code instead of simple `print(...)` statements. -Insert this at the top-level of your load.py file (so not inside -`plugin_start3()` ): +Insert this at the top-level of your load.py file (so not inside `plugin_start3()` ): ```python import logging @@ -145,7 +150,7 @@ Replace all `print(...)` statements with one of the following: logger.debug('Exception we only note in debug output', exc_info=e) ``` -Remember you can use fstrings to include variables, and even the returns of +Remember you can use [fstrings](https://www.python.org/dev/peps/pep-0498/) to include variables, and even the returns of functions, in the output. ```python @@ -160,11 +165,11 @@ EDMC will import the `load.py` file as a module and then call the `plugin_start3()` function. ```python -def plugin_start3(plugin_dir): +def plugin_start3(plugin_dir: str) -> str: """ Load this plugin into EDMC """ - print("I am loaded! My plugin folder is {}".format(plugin_dir)) + print(f"I am loaded! My plugin folder is {plugin_dir}") return "Test" ``` @@ -179,7 +184,7 @@ Mac, and `$TMP/EDMarketConnector.log` on Linux. This gets called when the user closes the program: ```python -def plugin_stop(): +def plugin_stop() -> None: """ EDMC is closing """ @@ -210,20 +215,23 @@ numbers in a locale-independent way. ```python import tkinter as tk +from tkinter import ttk import myNotebook as nb from config import config +from typing import Optional -this = sys.modules[__name__] # For holding module globals +my_setting: Optional[tk.IntVar] = None -def plugin_prefs(parent, cmdr, is_beta): +def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]: """ Return a TK Frame for adding to the EDMC settings dialog. """ - this.mysetting = tk.IntVar(value=config.getint("MyPluginSetting")) # Retrieve saved value from config + global my_setting + my_setting = tk.IntVar(value=config.getint("MyPluginSetting")) # Retrieve saved value from config frame = nb.Frame(parent) nb.Label(frame, text="Hello").grid() nb.Label(frame, text="Commander").grid() - nb.Checkbutton(frame, text="My Setting", variable=this.mysetting).grid() + nb.Checkbutton(frame, text="My Setting", variable=my_setting).grid() return frame ``` @@ -231,11 +239,11 @@ def plugin_prefs(parent, cmdr, is_beta): This gets called when the user dismisses the settings dialog: ```python -def prefs_changed(cmdr, is_beta): +def prefs_changed(cmdr: str, is_beta: bool) -> None: """ Save settings. """ - config.set('MyPluginSetting', this.mysetting.getint()) # Store new value in config + config.set('MyPluginSetting', my_setting.get()) # Store new value in config ``` ### Display @@ -251,19 +259,26 @@ You can use `stringFromNumber()` from EDMC's `l10n.Locale` object to format numbers in your widgets in a locale-independent way. ```python -this = sys.modules[__name__] # For holding module globals +from typing import Optional, Tuple +import tkinter as tk -def plugin_app(parent): +status: Optional[tk.Label] + + +def plugin_app(parent: tk.Frame) -> Tuple[tk.Label, tk.Label]: """ Create a pair of TK widgets for the EDMC main window """ + global status label = tk.Label(parent, text="Status:") # By default widgets inherit the current theme's colors - this.status = tk.Label(parent, text="", foreground="yellow") # Override theme's foreground color - return (label, this.status) + status = tk.Label(parent, text="", foreground="yellow") # Override theme's foreground color + return (label, status) # later on your event functions can update the contents of these widgets - this.status["text"] = "Happy!" - this.status["foreground"] = "green" +def some_other_function() -> None: + global status + status["text"] = "Happy!" + status["foreground"] = "green" ``` You can dynamically add and remove widgets on the main window by returning a @@ -551,7 +566,7 @@ plugin's folder: If there are any external dependencies then include them in the plugin's folder. -Optionally, for tidiness delete any `.pyc` and `.pyo` files in the archive. +Optionally, for tidiness delete any `.pyc` and `.pyo` files in the archive, as well as the `__pycache__` directory. ## Disable a plugin @@ -561,10 +576,14 @@ plugin folder to append ".disabled". Eg, Disabled and enabled plugins are listed on the "Plugins" Settings tab +## Glossary / Quick lookup + +TODO -- List of all functions / imports mentioned above with simple descriptions. + ## Migration Starting with pre-release 3.5 EDMC uses Python **3.7**. The first full -release under Python 3.7 will be 4.0.0.0. This is a brief outline of the steps +release under Python 3.7 was 4.0.0.0. This is a brief outline of the steps required to migrate a plugin from earlier versions of EDMC: - Rename the function `plugin_start` to `plugin_start3(plugin_dir)`. From 5ae269d7f658885d6512b6f7c9d2eb485e0b0bdd Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 12:25:36 +0200 Subject: [PATCH 53/71] Added type annotations to examples --- PLUGINS.md | 71 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index d86053df..9e679b96 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -250,7 +250,7 @@ def prefs_changed(cmdr: str, is_beta: bool) -> None: You can also have your plugin add an item to the EDMC main window and update from your event hooks. This works in the same way as `plugin_prefs()`. For a -simple one-line item return a tk.Label widget or a pair of widgets as a tuple. +simple one-line item return a `tk.Label` widget or a 2 tuple of widgets. For a more complicated item create a tk.Frame widget and populate it with other ttk widgets. Return `None` if you just want to use this as a callback after the main window and all other plugins are initialised. @@ -286,22 +286,27 @@ tk.Frame from `plugin_app()` and later creating and destroying child widgets of that frame. ```python +from typing import Option +import tkinter as tk + from theme import theme -this = sys.modules[__name__] # For holding module globals +frame: Optional[tk.Frame] = None -def plugin_app(parent): +def plugin_app(parent: tk.Frame) -> tk.Frame: """ Create a frame for the EDMC main window """ - this.frame = tk.Frame(parent) - return this.frame + global frame + frame = tk.Frame(parent) + return frame +def some_other_function_called_later() -> None: # later on your event functions can add or remove widgets - row = this.frame.grid_size()[1] - new_widget_1 = tk.Label(this.frame, text="Status:") + row = frame.grid_size()[1] + new_widget_1 = tk.Label(frame, text="Status:") new_widget_1.grid(row=row, column=0, sticky=tk.W) - new_widget_2 = tk.Label(this.frame, text="Unhappy!", foreground="red") # Override theme's foreground color + new_widget_2 = tk.Label(frame, text="Unhappy!", foreground="red") # Override theme's foreground color new_widget_2.grid(row=row, column=1, sticky=tk.W) theme.update(this.frame) # Apply theme colours to the frame and its children, including the new widgets ``` @@ -328,13 +333,16 @@ for an example of these techniques. #### Journal Entry ```python -def journal_entry(cmdr, is_beta, system, station, entry, state): +def journal_entry( + cmdr: str, is_beta: bool, system: str, station: str, entry: Dict[str, Any], state: Dict[str, Any] +) -> None: if entry['event'] == 'FSDJump': # We arrived at a new system! if 'StarPos' in entry: - sys.stderr.write("Arrived at {} ({},{},{})\n".format(entry['StarSystem'], *tuple(entry['StarPos']))) + logger.info(f'Arrived at {entry["StarSystem"]} {entry["StarPos"') + else: - sys.stderr.write("Arrived at {}\n".format(entry['StarSystem'])) + logger.info(f'Arrived at {entry["StarSystem"]}') ``` This gets called when EDMC sees a new entry in the game's journal. @@ -418,7 +426,10 @@ def cmdr_data(data, is_beta): """ We have new data on our commander """ - sys.stderr.write(data.get('commander') and data.get('commander').get('name') or '') + if data.get('commander') is None or data['commander'].get('name') is None: + raise ValueError("this isn't possible") + + logger.info(data['commander']['name']) ``` This gets called when EDMC has just fetched fresh Cmdr and station data from @@ -440,13 +451,16 @@ def edsm_notify_system(reply): `reply` holds the response from a call to https://www.edsm.net/en/api-journal-v1 """ if not reply: - sys.stderr.write("Error: Can't connect to EDSM\n") + logger.info("Error: Can't connect to EDSM") + elif reply['msgnum'] // 100 not in (1,4): - sys.stderr.write('Error: EDSM {MSG}\n').format(MSG=reply['msg']) + logger.info(f'Error: EDSM {reply["msg"]}') + elif reply.get('systemCreated'): - sys.stderr.write('New EDSM system!\n') + logger.info('New EDSM system!') + else: - sys.stderr.write('Known EDSM system\n') + logger.info('Known EDSM system') ``` If the player has chosen to "Send flight log and Cmdr status to EDSM" this gets @@ -461,19 +475,15 @@ def inara_notify_location(eventData): `eventData` holds the response to one of the "Commander's Flight Log" events https://inara.cz/inara-api-docs/#event-29 """ if eventData.get('starsystemInaraID'): - sys.stderr.write('Now in Inara system {ID} at {URL}\n'.format( - ID=eventData['starsystemInaraID'], - URL=eventData['starsystemInaraURL'] - )) + logging.info(f'Now in Inara system {eventData["starsystemInaraID"]} at {eventData["starsystemInaraURL"]}') else: - sys.stderr.write('System not known to Inara\n') + logger.info('System not known to Inara') + if eventData.get('stationInaraID'): - sys.stderr.write('Docked at Inara station {ID} at {URL}\n'.format( - ID=eventData['stationInaraID'], - URL=eventData['stationInaraURL'] - )) + logger.info(f'Docked at Inara station {eventData["stationInaraID"]} at {eventData["stationInaraURL"]}') + else: - sys.stderr.write('Undocked or station unknown to Inara\n') + logger.info('Undocked or station unknown to Inara') ``` If the player has chosen to "Send flight log and Cmdr status to Inara" this @@ -489,10 +499,9 @@ def inara_notify_ship(eventData): `eventData` holds the response to an addCommanderShip or setCommanderShip event https://inara.cz/inara-api-docs/#event-11 """ if eventData.get('shipInaraID'): - sys.stderr.write('Now in Inara ship {ID} at {URL}\n'.format( - ID=eventData['shipInaraID'], - URL=eventData['shipInaraURL'] - )) + logger.info( + f'Now in Inara ship {eventData['shipInaraID'],} at {eventData['shipInaraURL']} + ) ``` If the player has chosen to "Send flight log and Cmdr status to Inara" this @@ -526,7 +535,7 @@ _ = functools.partial(l10n.Translations.translate, context=__file__) Wrap each string that needs translating with the `_()` function, e.g.: ```python - this.status["text"] = _('Happy!') # Main window status + status["text"] = _('Happy!') # Main window status ``` If you display localized strings in EDMC's main window you should refresh them From 0577c08d54d1a452cd077c1e3b6b71f1e021f110 Mon Sep 17 00:00:00 2001 From: A_D Date: Fri, 18 Sep 2020 23:05:40 +0200 Subject: [PATCH 54/71] Fixed module docstring --- docs/examples/click_counter/load.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/click_counter/load.py b/docs/examples/click_counter/load.py index 7a4c9f49..02403f2f 100644 --- a/docs/examples/click_counter/load.py +++ b/docs/examples/click_counter/load.py @@ -1,7 +1,7 @@ """ Example EDMC plugin. -It adds a single button to the EDMC interface that displays the number of times it has been reset. +It adds a single button to the EDMC interface that displays the number of times it has been clicked. """ import logging @@ -13,7 +13,7 @@ from config import appname, config PLUGIN_NAME = "ClickCounter" -logger = logging.getLogger(f"{appname}") +logger = logging.getLogger(f"{appname}.{PLUGIN_NAME}") class ClickCounter: From 9c3f273027e8535db3e2a1ae457476228eb51f97 Mon Sep 17 00:00:00 2001 From: A_D Date: Mon, 21 Sep 2020 21:56:34 +0200 Subject: [PATCH 55/71] Added type hints and param descriptions --- PLUGINS.md | 168 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 96 insertions(+), 72 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 9e679b96..b93fb5bd 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -179,6 +179,11 @@ Any errors or print statements from your plugin will appear in `%TMP%\EDMarketConnector.log` on Windows, `$TMPDIR/EDMarketConnector.log` on Mac, and `$TMP/EDMarketConnector.log` on Linux. +| Parameter | Type | Description | +| :----------- | :---: | :------------------------------------------------------ | +| `plugin_dir` | `str` | The directory that your plugin is located in. | +| `RETURN` | `str` | The name you want to be used for your plugin internally | + ### Shutdown This gets called when the user closes the program: @@ -191,8 +196,8 @@ def plugin_stop() -> None: print("Farewell cruel world!") ``` -If your plugin uses one or more threads to handle Events then stop and join() -the threads before returning from this function. +If your plugin uses one or more threads to handle Events then `stop()` and `join()` +(to wait for their exit -- Recommended, not required) the threads before returning from this function. ## Plugin Hooks @@ -236,6 +241,12 @@ def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.F return frame ``` +| Parameter | Type | Description | +| :-------- | :-----------: | :----------------------------------------------- | +| `parent` | `nb.Notebook` | Root Notebook object the preferences window uses | +| `cmdr` | `str` | The current commander | +| `is_beta` | `bool` | If the game is currently a beta version | + This gets called when the user dismisses the settings dialog: ```python @@ -246,6 +257,11 @@ def prefs_changed(cmdr: str, is_beta: bool) -> None: config.set('MyPluginSetting', my_setting.get()) # Store new value in config ``` +| Parameter | Type | Description | +| :-------- | :----: | :-------------------------------------- | +| `cmdr` | `str` | The current commander | +| `is_beta` | `bool` | If the game is currently a beta version | + ### Display You can also have your plugin add an item to the EDMC main window and update @@ -281,9 +297,17 @@ def some_other_function() -> None: status["foreground"] = "green" ``` -You can dynamically add and remove widgets on the main window by returning a -tk.Frame from `plugin_app()` and later creating and destroying child widgets -of that frame. +| Parameter | Type | Description | +| :-------- | :---------------------------------------------: | :---------------------------------------------------------- | +| `parent` | `tk.Frame` | The root EDMC window | +| `RETURN` | `Union[tk.Widget, Tuple[tk.Widget, tk.Widget]]` | A widget to add to the main window. See below for more info | + +The return from plugin_app can either be any widget (Frame, Label, Notebook, etc.), or a 2 tuple of widgets. In the case of +a 2 tuple, indices 0 and 1 are placed automatically in the outer grid on column indices 0 and 1. Otherwise, the only thing done +to your return widget is it is set to use a columnspan of 2, and placed on the grid. + +You can dynamically add and remove widgets on the main window by returning a tk.Frame from `plugin_app()` and later +creating and destroying child widgets of that frame. ```python from typing import Option @@ -347,46 +371,40 @@ def journal_entry( This gets called when EDMC sees a new entry in the game's journal. -- `cmdr` is a `str` denoting the current Commander Name. -- `is_beta` is a `bool` denoting if data came from a beta version of the game. -- `system` is a `str` holding the name of the current system, or `None` if not - yet known. -- `station` is a `str` holding the name of the current station, or `None` if - not yet known or appropriate. -- `entry` is an `OrderedDict` holding the Journal event. -- `state` is a `dictionary` containing information about the Cmdr and their - ship and cargo (including the effect of the current journal entry). - - `Captain` - `str` of name of Commander's crew you joined in multi-crew, - else `None` - - `Cargo` - `dict` with details of current cargo. - - `Credits` - Current credit balance. - - `FID` - Frontier Cmdr ID - - `Horizons` - `bool` denoting if Horizons expansion active. - - `Loan` - Current loan amount, else None. - - `Raw` - `dict` with details of "Raw" materials held. - - `Manufactured` - `dict` with details of "Manufactured" materials held. - - `Encoded` - `dict` with details of "Encoded" materials held. - - `Engineers` - `dict` with details of Rank Progress for Engineers. - - `Rank` - `dict` of current Ranks. Each entry is a 2 `tuple` of ints, rank and age - - `Reputation` - `dict` of Major Faction reputations, scale is -100 to +100 - See Frontier's Journal Manual for detail of bands. - - `Statistics` - `dict` of a Journal "Statistics" event, i.e. data shown - in the statistics panel on the right side of the cockpit. See Frontier's - Journal Manual for details. - - `Role` - Crew role if multi-crewing in another Commander's ship: - - `None` - - "Idle" - - "FireCon" - - "FighterCon" - - `Friends` -`set` of online friends. - - `ShipID` - `int` that denotes Frontier internal ID for your current ship. - - `ShipIdent` - `str` of your current ship's textual ID (which you set). - - `ShipName` - `str` of your current ship's textual Name (which you set). - - `ShipType` - `str` of your current ship's model, e.g. "CobraMkIII". - - `HullValue` - `int` of current ship's credits value, excluding modules. - - `ModulesValue` - `int` of current ship's module's total credits value. - - `Rebuy` - `int` of current ship's rebuy cost in credits. - - `Modules` - `dict` with data on currently fitted modules. +| Parameter | Type | Description | +| :-------- | :--------------: | :--------------------------------------------------------------------- | +| `cmdr` | `str` | Current commander name | +| `is_beta` | `bool` | Is the game currently in beta | +| `system` | `Optional[str]` | Current system, if known | +| `station` | `Optional[str]` | Current station, if any | +| `entry` | `Dict[str, Any]` | The journal event | +| `state` | `Dict[str, Any]` | More info about the commander, their ship, and their cargo (see below) | + +Content of `state` (updated to the current journal entry): + +| Field | Type | Description | +| :------------- | :-------------------------: | :-------------------------------------------------------------------------------------------------------------- | +| `Captian` | `Optional[str]` | Name of the commander who's crew you're on, if any | +| `Cargo` | `dict` | Current cargo | +| `Credits` | `int` | Current credits balance | +| `FID` | `str` | Frontier commander ID | +| `Loan` | `Optional[int]` | Current loan amount, if any | +| `Raw` | `dict` | Current raw engineering materials | +| `Manufactured` | `dict` | Current manufactured engineering materials | +| `Encoded` | `dict` | Current encoded engineering materials | +| `Engineers` | `dict` | Current Raw engineering materials | +| `Rank` | `Dict[str, Tuple[int, int]` | Current ranks, each entry is a tuple of the current rank, and age | +| `Statistics` | `dict` | Contents of a Journal Statistics event, ie, data shown in the stats panel. See the Journal manual for more info | +| `Role` | `Optional[str]` | Current role if in multi-crew, one of `Idle`, `FireCon`, `FighterCon` | +| `Friends` | `set` | Currently online friend | +| `ShipID` | `int` | Frontier ID of current ship | +| `ShipIdent` | `str` | Current user-set ship ID | +| `ShipName` | `str` | Current user-set ship name | +| `ShipType` | `str` | Internal name for the current ship type | +| `HullValue` | `int` | Current ship value, excluding modules | +| `ModulesValue` | `int` | Value of the current ship's modules | +| `Rebuy` | `int` | Current ship's rebuy cost | +| `Modules` | `dict` | Currently fitted modules | A special "StartUp" entry is sent if EDMC is started while the game is already running. In this case you won't receive initial events such as "LoadGame", @@ -402,7 +420,7 @@ machine so you should not *rely* on receiving this event. ```python import plug -def dashboard_entry(cmdr, is_beta, entry): +def dashboard_entry(cmdr: str, is_beta: bool, entry: Dict[str, Any]): is_deployed = entry['Flags'] & plug.FlagsHardpointsDeployed sys.stderr.write("Hardpoints {}\n".format(is_deployed and "deployed" or "stowed")) ``` @@ -410,14 +428,13 @@ def dashboard_entry(cmdr, is_beta, entry): This gets called when something on the player's cockpit display changes - typically about once a second when in orbital flight. -- `cmdr` is a `str` denoting the current Commander Name. -- `is_beta` is a `bool` denoting if data came from a beta version of the game. -- `entry` is a `dict` loaded from the Status.json file the game writes. - See the "Status File" section in the Frontier [Journal documentation](https://forums.frontier.co.uk/showthread.php/401661) - for the available `entry` properties and for the list of available `"Flags"`. - Ask on the EDCD Discord server to be sure you have the latest version. - Refer to the source code of [plug.py](./plug.py) for the list of available - constants. +| Parameter | Type | Description | +| :-------- | :----: | :-------------------------------- | +| `cmdr` | `str` | Current command name | +| `is_beta` | `bool` | if the game is currently in beta | +| `entry` | `dict` | Data from status.json (see below) | + + For more info on `status.json`, See the "Status File" section in the Frontier [Journal documentation](https://forums.frontier.co.uk/showthread.php/401661) for the available `entry` properties and for the list of available `"Flags"`. Refer to the source code of [plug.py](./plug.py) for the list of available constants. #### Getting Commander Data @@ -435,13 +452,10 @@ def cmdr_data(data, is_beta): This gets called when EDMC has just fetched fresh Cmdr and station data from Frontier's servers. -- `data` is a dictionary containing the response from Frontier to a CAPI -`/profile` request, augmented with two extra keys: - - `marketdata` - contains the CAPI data from the `/market` endpoint, if - docked and the station has the commodities service. - - `shipdata` - contains the CAPI data from the `/shipyard` endpoint, if - docked and the station has the shipyard service. -- `is_beta` is a `bool` denoting if data came from a beta version of the game. +| Parameter | Type | Description | +| :-------- | :--------------: | :------------------------------------------------------------------------------------------------------- | +| `data` | `Dict[str, Any]` | `/profile` API response, with `/market` and `/shipyard` added under the keys `marketdata` and `shipdata` | +| `is_beta` | `bool` | If the game is currently in beta | #### Plugin-specific events @@ -467,20 +481,22 @@ If the player has chosen to "Send flight log and Cmdr status to EDSM" this gets called when the player starts the game or enters a new system. It is called some time after the corresponding `journal_entry()` event. ---- +| Parameter | Type | Description | +| :-------- | :--------------: | :--------------------------------------------------------------------------------------------- | +| `reply` | `Dict[str, Any]` | Response to an API call to [EDSM's journal API target](https://www.edsm.net/en/api-journal-v1) | ```python -def inara_notify_location(eventData): +def inara_notify_location(event_data): """ - `eventData` holds the response to one of the "Commander's Flight Log" events https://inara.cz/inara-api-docs/#event-29 + `event_data` holds the response to one of the "Commander's Flight Log" events https://inara.cz/inara-api-docs/#event-29 """ - if eventData.get('starsystemInaraID'): - logging.info(f'Now in Inara system {eventData["starsystemInaraID"]} at {eventData["starsystemInaraURL"]}') + if event_data.get('starsystemInaraID'): + logging.info(f'Now in Inara system {event_data["starsystemInaraID"]} at {event_data["starsystemInaraURL"]}') else: logger.info('System not known to Inara') - if eventData.get('stationInaraID'): - logger.info(f'Docked at Inara station {eventData["stationInaraID"]} at {eventData["stationInaraURL"]}') + if event_data.get('stationInaraID'): + logger.info(f'Docked at Inara station {event_data["stationInaraID"]} at {event_data["stationInaraURL"]}') else: logger.info('Undocked or station unknown to Inara') @@ -491,16 +507,20 @@ gets called when the player starts the game, enters a new system, docks or undocks. It is called some time after the corresponding `journal_entry()` event. +| Parameter | Type | Description | +| :----------- | :--------------: | :----------------------------------------------------------------------------------------------------------- | +| `event_data` | `Dict[str, Any]` | Response to an API call to [INARA's `Commander Flight Log` event](https://inara.cz/inara-api-docs/#event-29) | + --- ```python -def inara_notify_ship(eventData): +def inara_notify_ship(event_data): """ - `eventData` holds the response to an addCommanderShip or setCommanderShip event https://inara.cz/inara-api-docs/#event-11 + `event_data` holds the response to an addCommanderShip or setCommanderShip event https://inara.cz/inara-api-docs/#event-11 """ - if eventData.get('shipInaraID'): + if event_data.get('shipInaraID'): logger.info( - f'Now in Inara ship {eventData['shipInaraID'],} at {eventData['shipInaraURL']} + f'Now in Inara ship {event_data['shipInaraID'],} at {event_data['shipInaraURL']} ) ``` @@ -508,6 +528,10 @@ If the player has chosen to "Send flight log and Cmdr status to Inara" this gets called when the player starts the game or switches ship. It is called some time after the corresponding `journal_entry()` event. +| Parameter | Type | Description | +| :----------- | :--------------: | :----------------------------------------------------------------------------------------------------------------------------- | +| `event_data` | `Dict[str, Any]` | Response to an API call to [INARA's `addCommanderShip` or `setCommanderShip` event](https://inara.cz/inara-api-docs/#event-11) | + ## Error messages You can display an error in EDMC's status area by returning a string from your From 97d8631e1174f25a674937309175985b9fc33a90 Mon Sep 17 00:00:00 2001 From: A_D Date: Tue, 22 Sep 2020 15:22:27 +0200 Subject: [PATCH 56/71] Removed todo --- PLUGINS.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index b93fb5bd..717171f7 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -609,10 +609,6 @@ plugin folder to append ".disabled". Eg, Disabled and enabled plugins are listed on the "Plugins" Settings tab -## Glossary / Quick lookup - -TODO -- List of all functions / imports mentioned above with simple descriptions. - ## Migration Starting with pre-release 3.5 EDMC uses Python **3.7**. The first full From 803a6c31c9ab0dbd954f3d4400155663fabf21c8 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 11 Sep 2020 14:40:39 +0100 Subject: [PATCH 57/71] miggytest plugin: Tests "for plugins" modules This plugin is designed as a quick test that we are supplying the things we say we will for plugins. * Check that modules are available: sqlite3, shutil, zipfile. * Check that logging works as expected. --- plugins/miggytest.disabled/load.py | 86 ++++++++++++++++++++ plugins/miggytest.disabled/miggytest.db | Bin 0 -> 61440 bytes plugins/miggytest.disabled/subA/__init__.py | 6 ++ 3 files changed, 92 insertions(+) create mode 100644 plugins/miggytest.disabled/load.py create mode 100644 plugins/miggytest.disabled/miggytest.db create mode 100644 plugins/miggytest.disabled/subA/__init__.py diff --git a/plugins/miggytest.disabled/load.py b/plugins/miggytest.disabled/load.py new file mode 100644 index 00000000..23e0dd41 --- /dev/null +++ b/plugins/miggytest.disabled/load.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# vim: textwidth=0 wrapmargin=0 tabstop=4 shiftwidth=4 softtabstop=4 smartindent smarttab + +import os +import sys +import sqlite3 +import shutil +import zipfile +import logging + +from config import appname + +from subA import subA + +# This could also be returned from plugin_start3() +plugin_name = os.path.basename(os.path.dirname(__file__)) + +# Logger per found plugin, so the folder name is included in +# the logging format. +logger = logging.getLogger(f'{appname}.{plugin_name}') +if not logger.hasHandlers(): + level = logging.INFO # So logger.info(...) is equivalent to print() + + logger.setLevel(level) + logger_channel = logging.StreamHandler() + logger_channel.setLevel(level) + logger_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d:%(funcName)s: %(message)s') # noqa: E501 + logger_formatter.default_time_format = '%Y-%m-%d %H:%M:%S' + logger_formatter.default_msec_format = '%s.%03d' + logger_channel.setFormatter(logger_formatter) + logger.addHandler(logger_channel) + + +this = sys.modules[__name__] # For holding module globals +this.DBFILE = 'miggytest.db' +this.mt = None + + +class miggytest(object): + def __init__(self, directory: str): + logger.debug(f'directory = "{directory}') + dbfile = os.path.join(directory, this.DBFILE) + + # Test 'import zipfile' + with zipfile.ZipFile(dbfile + '.zip', 'w') as zip: + if os.path.exists(dbfile): + zip.write(dbfile) + zip.close() + + # Testing 'import shutil' + if os.path.exists(dbfile): + shutil.copyfile(dbfile, dbfile + '.bak') + + # Testing 'import sqlite3' + self.sqlconn = sqlite3.connect(dbfile) + self.sqlc = self.sqlconn.cursor() + try: + self.sqlc.execute('CREATE TABLE entries (timestamp TEXT, cmdrname TEXT, system TEXT, station TEXT, eventtype TEXT)') # noqa: E501 + except sqlite3.OperationalError: + logger.exception('sqlite3.OperationalError when CREATE TABLE entries:') + + def store(self, timestamp: str, cmdrname: str, system: str, station: str, event: str): + logger.debug(f'timestamp = "{timestamp}", cmdr = "{cmdrname}", system = "{system}", station = "{station}", event = "{event}"') # noqa: E501 + self.sqlc.execute('INSERT INTO entries VALUES(?, ?, ?, ?, ?)', (timestamp, cmdrname, system, station, event)) + self.sqlconn.commit() + return None + + +def plugin_start3(plugin_dir: str): + logger.info(f'Folder is {plugin_dir}') + this.mt = miggytest(plugin_dir) + + this.suba = subA(logger) + + this.suba.ping() + + return plugin_name + + +def plugin_stop(): + logger.info('Stopping') + + +def journal_entry(cmdrname: str, is_beta: bool, system: str, station: str, entry: dict, state: dict): + logger.debug(f'cmdr = "{cmdrname}", is_beta = "{is_beta}", system = "{system}", station = "{station}"') + this.mt.store(entry['timestamp'], cmdrname, system, station, entry['event']) diff --git a/plugins/miggytest.disabled/miggytest.db b/plugins/miggytest.disabled/miggytest.db new file mode 100644 index 0000000000000000000000000000000000000000..daf421ce9896ff5f2989a3e8c1647104e2366020 GIT binary patch literal 61440 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 NXm*GWmOehj{{fs>*_QwS literal 0 HcmV?d00001 diff --git a/plugins/miggytest.disabled/subA/__init__.py b/plugins/miggytest.disabled/subA/__init__.py new file mode 100644 index 00000000..acd1a2cb --- /dev/null +++ b/plugins/miggytest.disabled/subA/__init__.py @@ -0,0 +1,6 @@ +class subA: + def __init__(self, logger): + self.logger = logger + + def ping(cls): + cls.logger.info('ping!') From ec5b9fc56114e9c095218fba4259c57f9de22b61 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 14:43:10 +0100 Subject: [PATCH 58/71] Rename miggytest plugin to plugintest. Also now has docstrings and types, and generally passes flake8 and also mypy, with the exception of that 'this' hack not being liked. --- docs/examples/plugintest/SubA/__init__.py | 18 +++++ .../examples/plugintest}/load.py | 69 +++++++++++++----- .../examples/plugintest}/miggytest.db | Bin plugins/miggytest.disabled/subA/__init__.py | 6 -- 4 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 docs/examples/plugintest/SubA/__init__.py rename {plugins/miggytest.disabled => docs/examples/plugintest}/load.py (71%) rename {plugins/miggytest.disabled => docs/examples/plugintest}/miggytest.db (100%) delete mode 100644 plugins/miggytest.disabled/subA/__init__.py diff --git a/docs/examples/plugintest/SubA/__init__.py b/docs/examples/plugintest/SubA/__init__.py new file mode 100644 index 00000000..1630863a --- /dev/null +++ b/docs/examples/plugintest/SubA/__init__.py @@ -0,0 +1,18 @@ +"""Test class logging.""" + +import logging + + +class SubA: + """Simple class to test logging.""" + + def __init__(self, logger: logging.Logger): + self.logger = logger + + def ping(self) -> None: + """ + Log a ping to demonstrate correct logging. + + :return: + """ + self.logger.info('ping!') diff --git a/plugins/miggytest.disabled/load.py b/docs/examples/plugintest/load.py similarity index 71% rename from plugins/miggytest.disabled/load.py rename to docs/examples/plugintest/load.py index 23e0dd41..4a614aa2 100644 --- a/plugins/miggytest.disabled/load.py +++ b/docs/examples/plugintest/load.py @@ -1,17 +1,18 @@ #!/usr/bin/env python # vim: textwidth=0 wrapmargin=0 tabstop=4 shiftwidth=4 softtabstop=4 smartindent smarttab +"""Plugin that tests that modules we bundle for plugins are present and working.""" -import os -import sys -import sqlite3 -import shutil -import zipfile import logging +import os +import shutil +import sqlite3 +import sys +import zipfile + +from SubA import SubA from config import appname -from subA import subA - # This could also be returned from plugin_start3() plugin_name = os.path.basename(os.path.dirname(__file__)) @@ -31,12 +32,14 @@ if not logger.hasHandlers(): logger.addHandler(logger_channel) -this = sys.modules[__name__] # For holding module globals -this.DBFILE = 'miggytest.db' +this = sys.modules[__name__] # For holding module globals +this.DBFILE = 'plugintest.db' this.mt = None -class miggytest(object): +class PluginTest(object): + """Class that performs actual tests on bundled modules.""" + def __init__(self, directory: str): logger.debug(f'directory = "{directory}') dbfile = os.path.join(directory, this.DBFILE) @@ -59,28 +62,60 @@ class miggytest(object): except sqlite3.OperationalError: logger.exception('sqlite3.OperationalError when CREATE TABLE entries:') - def store(self, timestamp: str, cmdrname: str, system: str, station: str, event: str): + def store(self, timestamp: str, cmdrname: str, system: str, station: str, event: str) -> None: + """ + Store the provided data in sqlite database. + + :param timestamp: + :param cmdrname: + :param system: + :param station: + :param event: + :return: None + """ logger.debug(f'timestamp = "{timestamp}", cmdr = "{cmdrname}", system = "{system}", station = "{station}", event = "{event}"') # noqa: E501 self.sqlc.execute('INSERT INTO entries VALUES(?, ?, ?, ?, ?)', (timestamp, cmdrname, system, station, event)) self.sqlconn.commit() return None -def plugin_start3(plugin_dir: str): - logger.info(f'Folder is {plugin_dir}') - this.mt = miggytest(plugin_dir) +def plugin_start3(plugin_dir: str) -> str: + """ + Plugin startup method. - this.suba = subA(logger) + :param plugin_dir: + :return: 'Pretty' name of this plugin. + """ + logger.info(f'Folder is {plugin_dir}') + this.mt = PluginTest(plugin_dir) + + this.suba = SubA(logger) this.suba.ping() return plugin_name -def plugin_stop(): +def plugin_stop() -> None: + """ + Plugin stop method. + + :return: + """ logger.info('Stopping') -def journal_entry(cmdrname: str, is_beta: bool, system: str, station: str, entry: dict, state: dict): +def journal_entry(cmdrname: str, is_beta: bool, system: str, station: str, entry: dict, state: dict) -> None: + """ + Handle the given journal entry. + + :param cmdrname: + :param is_beta: + :param system: + :param station: + :param entry: + :param state: + :return: None + """ logger.debug(f'cmdr = "{cmdrname}", is_beta = "{is_beta}", system = "{system}", station = "{station}"') this.mt.store(entry['timestamp'], cmdrname, system, station, entry['event']) diff --git a/plugins/miggytest.disabled/miggytest.db b/docs/examples/plugintest/miggytest.db similarity index 100% rename from plugins/miggytest.disabled/miggytest.db rename to docs/examples/plugintest/miggytest.db diff --git a/plugins/miggytest.disabled/subA/__init__.py b/plugins/miggytest.disabled/subA/__init__.py deleted file mode 100644 index acd1a2cb..00000000 --- a/plugins/miggytest.disabled/subA/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -class subA: - def __init__(self, logger): - self.logger = logger - - def ping(cls): - cls.logger.info('ping!') From a308171bfcb2ee5deec9226de24648978ecc3520 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 14:50:27 +0100 Subject: [PATCH 59/71] Remove plugintest/miggytest.db I swear I already did this .... --- docs/examples/plugintest/miggytest.db | Bin 61440 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/examples/plugintest/miggytest.db diff --git a/docs/examples/plugintest/miggytest.db b/docs/examples/plugintest/miggytest.db deleted file mode 100644 index daf421ce9896ff5f2989a3e8c1647104e2366020..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61440 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 NXm*GWmOehj{{fs>*_QwS From 9e1ffc14c8240be9973c37202b8a18024f6e9eb3 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 21 Sep 2020 19:20:08 +0100 Subject: [PATCH 60/71] setup.py: Reference EDMC.manifest ready for its creation We'll want to do the same "use the UTF-8 codepage" thing here. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a3e3d397..4578b3cc 100755 --- a/setup.py +++ b/setup.py @@ -172,6 +172,7 @@ setup( 'version': BASEVERSION, 'product_version': VERSION, 'copyright': COPYRIGHT, + 'other_resources': [(24, 1, open(APPCMDNAME+'.manifest').read())], } ], data_files = DATA_FILES, options = OPTIONS, From 0f3ebd6e23975a73d6c9a655c500685f08018083 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 15:34:35 +0100 Subject: [PATCH 61/71] Add a manifest for EDMC.exe so that it also runs UTF-8 --- EDMC.manifest | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 EDMC.manifest diff --git a/EDMC.manifest b/EDMC.manifest new file mode 100644 index 00000000..9b412178 --- /dev/null +++ b/EDMC.manifest @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + UTF-8 + + + + + + + + + + From c0b01f88acd886889628deec543a46578817eb0c Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 15:35:42 +0100 Subject: [PATCH 62/71] Add get_main_logger() method to EDMCLogging.py This is so the decision on appname versus appcmdname is in one place. --- EDMCLogging.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/EDMCLogging.py b/EDMCLogging.py index 62aab91a..514a38f0 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -10,13 +10,14 @@ strings. import inspect import logging import logging.handlers +import os import pathlib import tempfile # So that any warning about accessing a protected member is only in one place. from sys import _getframe as getframe from typing import Tuple -from config import appname, config +from config import appcmdname, appname, config # TODO: Tests: # @@ -327,6 +328,17 @@ class EDMCContextFilter(logging.Filter): return module_name +def get_main_logger() -> logging.Logger: + """Return the correct logger for how the program is being run.""" + + if not os.getenv("EDMC_NO_UI"): + # GUI app being run + return logging.getLogger(appname) + else: + # Must be the CLI + return logging.getLogger(appcmdname) + + # Singleton loglevel = config.get('loglevel') if not loglevel: From edc6ed6596182815e89956e5d8e52bd8baf2908a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 15:37:45 +0100 Subject: [PATCH 63/71] Add locale DEBUG logging to EDMC.py --- EDMC.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/EDMC.py b/EDMC.py index 8ef2e78c..2d2ec3b9 100755 --- a/EDMC.py +++ b/EDMC.py @@ -6,6 +6,7 @@ import argparse import json +import locale import logging import os import re @@ -14,6 +15,9 @@ from os.path import getmtime, join from time import sleep, time from typing import Any, Optional +# workaround for https://github.com/EDCD/EDMarketConnector/issues/568 +os.environ["EDMC_NO_UI"] = "1" + import collate import commodity import companion @@ -36,14 +40,31 @@ sys.path.append(config.internal_plugin_dir) import eddn # noqa: E402 # isort: on -# workaround for https://github.com/EDCD/EDMarketConnector/issues/568 -os.environ["EDMC_NO_UI"] = "1" - -l10n.Translations.install_dummy() - logger = EDMCLogging.Logger(appcmdname).get_logger() logger.setLevel(logging.INFO) +logger.debug(f'Startup v{appversion} : Running on Python v{sys.version}') +logger.debug(f'''Platform: {sys.platform} +argv[0]: {sys.argv[0]} +exec_prefix: {sys.exec_prefix} +executable: {sys.executable} +sys.path: {sys.path}''' + ) + + +def log_locale(prefix: str) -> None: + logger.debug(f'''Locale: {prefix} +Locale LC_COLLATE: {locale.getlocale(locale.LC_COLLATE)} +Locale LC_CTYPE: {locale.getlocale(locale.LC_CTYPE)} +Locale LC_MONETARY: {locale.getlocale(locale.LC_MONETARY)} +Locale LC_NUMERIC: {locale.getlocale(locale.LC_NUMERIC)} +Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}''' + ) + + +log_locale('Initial Locale') + +l10n.Translations.install_dummy() SERVER_RETRY = 5 # retry pause for Companion servers [s] EXIT_SUCCESS, EXIT_SERVER, EXIT_CREDENTIALS, EXIT_VERIFICATION, EXIT_LAGGING, EXIT_SYS_ERR, EXIT_ARGS = range(7) From 22b3362c2076b96ddffffa539c5f3279365e0ac7 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 15:37:56 +0100 Subject: [PATCH 64/71] companion.py: Switch to using EDMCLogging.get_main_logger() So that we use the correct one for GUI versus CLI. --- companion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/companion.py b/companion.py index 3be504f4..ee6369eb 100644 --- a/companion.py +++ b/companion.py @@ -9,7 +9,6 @@ protocol used for the callback. import base64 import csv import hashlib -import logging import numbers import os import random @@ -26,9 +25,10 @@ from typing import TYPE_CHECKING, Any, Dict, List, NewType, Union import requests from config import appname, appversion, config +from EDMCLogging import get_main_logger from protocol import protocolhandler -logger = logging.getLogger(appname) +logger = get_main_logger() if TYPE_CHECKING: _ = lambda x: x # noqa: E731 # to make flake8 stop complaining that the hacked in _ method doesnt exist From a883eb29b277ccf95f8257d7385bec15575ad657 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 15:44:16 +0100 Subject: [PATCH 65/71] plug.py, prefs.py: Use get_main_logger() --- plug.py | 4 ++-- prefs.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plug.py b/plug.py index 51d73531..a13d93b2 100644 --- a/plug.py +++ b/plug.py @@ -9,15 +9,15 @@ import sys import operator import threading # noqa: F401 - We don't use it, but plugins might from typing import Optional -import logging import tkinter as tk import myNotebook as nb # noqa: N813 from config import config, appname +from EDMCLogging import get_main_logger import logging -logger = logging.getLogger(appname) +logger = get_main_logger() # Dashboard Flags constants FlagsDocked = 1 << 0 # on a landing pad diff --git a/prefs.py b/prefs.py index cdc0ab3c..28a7553f 100644 --- a/prefs.py +++ b/prefs.py @@ -9,16 +9,15 @@ from tkinter import colorchooser as tkColorChooser from ttkHyperlinkLabel import HyperlinkLabel import myNotebook as nb -from config import appname, applongname, config, appversion +from config import applongname, config, appversion from hotkey import hotkeymgr from l10n import Translations from monitor import monitor from theme import theme import plug -import logging -logger = logging.getLogger(appname) -from EDMCLogging import edmclogger +from EDMCLogging import edmclogger, get_main_logger +logger = get_main_logger() ########################################################################### # Versioned preferences, so we know whether to set an 'on' default on From 3759f2f0f21c1115ba64d2aa77d98f47f45b3cd4 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 15:48:09 +0100 Subject: [PATCH 66/71] Use appropriate base logger name for plugins. --- plug.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plug.py b/plug.py index a13d93b2..10e8957d 100644 --- a/plug.py +++ b/plug.py @@ -13,7 +13,7 @@ import tkinter as tk import myNotebook as nb # noqa: N813 -from config import config, appname +from config import appcmdname, appname, config from EDMCLogging import get_main_logger import logging @@ -203,7 +203,11 @@ def load_plugins(master): # Create a logger for this 'found' plugin. Must be before the # load.py is loaded. import EDMCLogging - plugin_logger = EDMCLogging.get_plugin_logger(f'{appname}.{name}') + if not os.getenv('EDMC_NO_UI'): + base_logger_name = appname + else: + base_logger_name = appcmdname + plugin_logger = EDMCLogging.get_plugin_logger(f'{base_logger_name}.{name}') found.append(Plugin(name, os.path.join(config.plugin_dir, name, 'load.py'), plugin_logger)) except Exception as e: From 14b8565aff84ba74c1510f650b553c90efb96c2d Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 15:50:06 +0100 Subject: [PATCH 67/71] Add 'import logging' (back in?) to prefs.py --- prefs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/prefs.py b/prefs.py index 28a7553f..fb87a639 100644 --- a/prefs.py +++ b/prefs.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import logging from os.path import dirname, expanduser, expandvars, exists, isdir, join, normpath from sys import platform import webbrowser From fa326ad3d373e99d7f4cac32a633d3826f7a18a1 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 15:54:18 +0100 Subject: [PATCH 68/71] EDMC.py: Move DEBUG logging to after level is set. --- EDMC.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/EDMC.py b/EDMC.py index 2d2ec3b9..d1f9411d 100755 --- a/EDMC.py +++ b/EDMC.py @@ -43,14 +43,6 @@ import eddn # noqa: E402 logger = EDMCLogging.Logger(appcmdname).get_logger() logger.setLevel(logging.INFO) -logger.debug(f'Startup v{appversion} : Running on Python v{sys.version}') -logger.debug(f'''Platform: {sys.platform} -argv[0]: {sys.argv[0]} -exec_prefix: {sys.exec_prefix} -executable: {sys.executable} -sys.path: {sys.path}''' - ) - def log_locale(prefix: str) -> None: logger.debug(f'''Locale: {prefix} @@ -62,8 +54,6 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}''' ) -log_locale('Initial Locale') - l10n.Translations.install_dummy() SERVER_RETRY = 5 # retry pause for Companion servers [s] @@ -133,7 +123,15 @@ def main(): sys.exit(EXIT_ARGS) logger.setLevel(args.loglevel) - logger.debug('Startup') + logger.debug(f'Startup v{appversion} : Running on Python v{sys.version}') + logger.debug(f'''Platform: {sys.platform} +argv[0]: {sys.argv[0]} +exec_prefix: {sys.exec_prefix} +executable: {sys.executable} +sys.path: {sys.path}''' + ) + + log_locale('Initial Locale') if args.j: logger.debug('Import and collate from JSON dump') From 7750bbdf4a4f83e371ab477e983de313acf04464 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 16:09:39 +0100 Subject: [PATCH 69/71] Move GUI/CLI conditional into get_plugin_logger() It's cleaner here than in the calling plug.py code. --- EDMCLogging.py | 9 +++++++-- plug.py | 6 +----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/EDMCLogging.py b/EDMCLogging.py index 514a38f0..f882f924 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -123,7 +123,7 @@ class Logger: return self.logger_channel -def get_plugin_logger(name: str, loglevel: int = _default_loglevel) -> logging.Logger: +def get_plugin_logger(plugin_name: str, loglevel: int = _default_loglevel) -> logging.Logger: """ Return a logger suitable for a plugin. @@ -144,7 +144,12 @@ def get_plugin_logger(name: str, loglevel: int = _default_loglevel) -> logging.L :param loglevel: Optional logLevel for this Logger. :return: logging.Logger instance, all set up. """ - plugin_logger = logging.getLogger(name) + if not os.getenv('EDMC_NO_UI'): + base_logger_name = appname + else: + base_logger_name = appcmdname + + plugin_logger = logging.getLogger(f'{base_logger_name}.{plugin_name}') plugin_logger.setLevel(loglevel) plugin_logger.addFilter(EDMCContextFilter()) diff --git a/plug.py b/plug.py index 10e8957d..f3df95eb 100644 --- a/plug.py +++ b/plug.py @@ -203,12 +203,8 @@ def load_plugins(master): # Create a logger for this 'found' plugin. Must be before the # load.py is loaded. import EDMCLogging - if not os.getenv('EDMC_NO_UI'): - base_logger_name = appname - else: - base_logger_name = appcmdname - plugin_logger = EDMCLogging.get_plugin_logger(f'{base_logger_name}.{name}') + plugin_logger = EDMCLogging.get_plugin_logger(name) found.append(Plugin(name, os.path.join(config.plugin_dir, name, 'load.py'), plugin_logger)) except Exception as e: logger.exception(f'Failure loading found Plugin "{name}"') From a3b69fd94d23746adccc034570bc2ea8efdd6445 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 16:20:05 +0100 Subject: [PATCH 70/71] Switch internal plugins to EDMCLogging.get_main_logger() --- plugins/eddn.py | 6 +++--- plugins/edsm.py | 6 +++--- plugins/inara.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/eddn.py b/plugins/eddn.py index 2001d321..5e7f27fe 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -2,7 +2,6 @@ import itertools import json -import logging import pathlib import re import sys @@ -19,7 +18,8 @@ import requests import myNotebook as nb # noqa: N813 from companion import category_map -from config import applongname, appname, appversion, config +from config import applongname, appversion, config +from EDMCLogging import get_main_logger from myNotebook import Frame from prefs import prefsVersion from ttkHyperlinkLabel import HyperlinkLabel @@ -32,7 +32,7 @@ if TYPE_CHECKING: def _(x: str) -> str: return x -logger = logging.getLogger(appname) +logger = get_main_logger() this: Any = sys.modules[__name__] # For holding module globals diff --git a/plugins/edsm.py b/plugins/edsm.py index 2bc42b4c..b57575a3 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -10,7 +10,6 @@ # text is always fired. i.e. CAPI cmdr_data() processing. import json -import logging import sys import tkinter as tk from queue import Queue @@ -21,14 +20,15 @@ import requests import myNotebook as nb # noqa: N813 import plug -from config import applongname, appname, appversion, config +from config import applongname, appversion, config +from EDMCLogging import get_main_logger from ttkHyperlinkLabel import HyperlinkLabel if TYPE_CHECKING: def _(x: str) -> str: return x -logger = logging.getLogger(appname) +logger = get_main_logger() EDSM_POLL = 0.1 _TIMEOUT = 20 diff --git a/plugins/inara.py b/plugins/inara.py index e1b1efd8..4b61322c 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -2,7 +2,6 @@ import dataclasses import json -import logging import sys import time import tkinter as tk @@ -22,10 +21,11 @@ import requests import myNotebook as nb # noqa: N813 import plug import timeout_session -from config import applongname, appname, appversion, config +from config import applongname, appversion, config +from EDMCLogging import get_main_logger from ttkHyperlinkLabel import HyperlinkLabel -logger = logging.getLogger(appname) +logger = get_main_logger() if TYPE_CHECKING: def _(x: str) -> str: @@ -1178,7 +1178,7 @@ def new_add_event( api_key = credentials(this.cmdr) if api_key is None: - logger.warn(f"cannot find an API key for cmdr {this.cmdr!r}") + logger.warning(f"cannot find an API key for cmdr {this.cmdr!r}") return key = Credentials(str(cmdr), str(fid), api_key) # this fails type checking due to `this` weirdness, hence str() From 4cec46b0282ee768c3ee3dae227be98bdbc29471 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 22 Sep 2020 16:30:49 +0100 Subject: [PATCH 71/71] Pre-Release 4.1.0-beta6 --- ChangeLog.md | 16 ++++++++++++++++ config.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 4245028f..dbeaeb09 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,22 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are in reverse chronological order (latest first). --- +Pre-Release 4.1.0-beta6 +=== + +* Include a manifest in both EDMarketConnector.exe and EDMC.exe to specify + they're Unicode applications so that they default to using the UTF-8 + codepage. Fixes [#711](https://github.com/EDCD/EDMarketConnector/issues/711). + +* Add extra debug logging to EDSM plugin for `CarrierJump`, `FSDJump`, + `Location` and `Docked` events. Hopefully this will help track down the + cause of [#713](https://github.com/EDCD/EDMarketConnector/issues/713). + +* Various code cleanups which shouldn't impact user experience. + +* EDMC.exe will now log useful startup state information if run with the + `--loglevel DEBUG` arguments. + Pre-Release 4.1.0-beta5 === diff --git a/config.py b/config.py index 10d4b471..2fb400e3 100644 --- a/config.py +++ b/config.py @@ -13,7 +13,7 @@ appcmdname = 'EDMC' # appversion **MUST** follow Semantic Versioning rules: # # Major.Minor.Patch(-prerelease)(+buildmetadata) -appversion = '4.1.0-beta5' #-rc1+a872b5f' +appversion = '4.1.0-beta6' #-rc1+a872b5f' # For some things we want appversion without (possible) +build metadata appversion_nobuild = str(semantic_version.Version(appversion).truncate('prerelease')) copyright = u'© 2015-2019 Jonathan Harris, 2020 EDCD'