1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-05-01 16:01:30 +03:00

Merge pull request #2408 from HullSeals/enhancement/2406/common-core-refactor

[#2406] Duplicate Code Refactor
This commit is contained in:
David Sangrey 2025-04-22 19:10:18 -04:00 committed by GitHub
commit ed964b3e55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 363 additions and 308 deletions

23
EDMC.py
View File

@ -10,12 +10,12 @@ from __future__ import annotations
import argparse
import json
import locale
import os
import queue
import sys
from time import sleep, time
from typing import TYPE_CHECKING, Any
from common_utils import log_locale, SERVER_RETRY
# isort: off
os.environ["EDMC_NO_UI"] = "1"
@ -52,31 +52,12 @@ import eddn # noqa: E402
# isort: on
def log_locale(prefix: str) -> None:
"""Log the current state of locale settings."""
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)}'''
)
tr.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, \
EXIT_JOURNAL_READ_ERR, EXIT_COMMANDER_UNKNOWN = range(9)
def versioncmp(versionstring) -> list:
"""Quick and dirty version comparison assuming "strict" numeric only version numbers."""
return list(map(int, versionstring.split('.')))
def deep_get(target: dict | companion.CAPIData, *args: str, default=None) -> Any:
"""
Walk into a dict and return the specified deep value.
@ -108,7 +89,7 @@ def deep_get(target: dict | companion.CAPIData, *args: str, default=None) -> Any
return current
def main(): # noqa: C901, CCR001
def main() -> None: # noqa: C901, CCR001
"""Run the main code of the program."""
try:
# arg parsing

View File

@ -335,8 +335,6 @@ class EDMCContextFilter(logging.Filter):
# <https://stackoverflow.com/questions/2203424/python-how-to-retrieve-class-information-from-a-frame-object#2220759>
try:
frame_info = inspect.getframeinfo(frame)
# raise(IndexError) # TODO: Remove, only for testing
except Exception:
# Separate from the print below to guarantee we see at least this much.
print('EDMCLogging:EDMCContextFilter:caller_attributes(): Failed in `inspect.getframinfo(frame)`')

View File

@ -38,17 +38,17 @@ from monitor import monitor
from EDMCLogging import get_main_logger
def get_sys_report(config: config.AbstractConfig) -> str:
def get_sys_report(active_config: config.AbstractConfig) -> str:
"""Gather system information about Elite, the Host Computer, and EDMC."""
# Calculate Requested Information
plt = platform.uname()
locale.setlocale(locale.LC_ALL, "")
lcl = locale.getlocale()
monitor.currentdir = config.get_str(
"journaldir", default=config.default_journal_dir
monitor.currentdir = active_config.get_str(
"journaldir", default=active_config.default_journal_dir
)
if not monitor.currentdir:
monitor.currentdir = config.default_journal_dir
monitor.currentdir = active_config.default_journal_dir
try:
logfile = monitor.journal_newest_filename(monitor.currentdir)
if logfile is None:
@ -108,21 +108,21 @@ def copy_sys_report(root: tk.Tk, report: str) -> None:
messagebox.showinfo("System Profiler", "System Report copied to Clipboard", parent=root)
def main() -> None:
def main(active_config: config.AbstractConfig) -> None:
"""Entry Point for the System Profiler."""
# Now Let's Begin
root: tk.Tk = tk.Tk()
root.withdraw() # Hide the window initially to calculate the dimensions
try:
icon_image = tk.PhotoImage(
file=path.join(cur_config.respath_path, "io.edcd.EDMarketConnector.png")
file=path.join(active_config.respath_path, "io.edcd.EDMarketConnector.png")
)
root.iconphoto(True, icon_image)
except tk.TclError:
root.iconbitmap(path.join(cur_config.respath_path, "EDMarketConnector.ico"))
root.iconbitmap(path.join(active_config.respath_path, "EDMarketConnector.ico"))
sys_report = get_sys_report(cur_config)
sys_report = get_sys_report(active_config)
# Set up styling
style = ttk.Style(root)
@ -182,7 +182,8 @@ if __name__ == "__main__":
# Args: Only work if not frozen
parser = argparse.ArgumentParser(
prog=appname,
description="Prints diagnostic and debugging information about the current EDMC configuration.",
description="Prints diagnostic and debugging information "
"about the current EDMC configuration.",
)
parser.add_argument(
"--out-console",
@ -203,4 +204,4 @@ if __name__ == "__main__":
print(sys_report)
sys.exit(0)
main()
main(cur_config)

View File

@ -68,6 +68,7 @@ from config import appversion, appversion_nobuild, config, copyright
from EDMCLogging import edmclogger, logger, logging
from journal_lock import JournalLock, JournalLockResult
from update import check_for_fdev_updates
from common_utils import log_locale, SERVER_RETRY
if __name__ == '__main__': # noqa: C901
# Command-line arguments
@ -416,8 +417,6 @@ from monitor import monitor
from theme import theme
from ttkHyperlinkLabel import HyperlinkLabel, SHIPYARD_HTML_TEMPLATE
SERVER_RETRY = 5 # retry pause for Companion servers [s]
class AppWindow:
"""Define the main application window."""
@ -837,8 +836,7 @@ class AppWindow:
r' if downgrading between major versions with significant changes.\r\n\r\n'
'Do you want to open GitHub to download the latest release?'
)
update_msg = update_msg.replace('\\n', '\n')
update_msg = update_msg.replace('\\r', '\r')
update_msg = update_msg.replace('\\n', '\n').replace('\\r', '\r')
stable_popup = tk.messagebox.askyesno(title=title, message=update_msg)
if stable_popup:
webbrowser.open("https://github.com/EDCD/eDMarketConnector/releases/latest")
@ -1979,17 +1977,6 @@ def test_logging() -> None:
logger.debug('Test from EDMarketConnector.py top-level test_logging()')
def log_locale(prefix: str) -> None:
"""Log all of the current local settings."""
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)}'''
)
def setup_killswitches(filename: str | None):
"""Download and setup the main killswitch list."""
logger.debug('fetching killswitches...')
@ -2061,17 +2048,15 @@ def validate_providers():
# LANG: Popup-text about Reset Providers
popup_text = tr.tl(r'One or more of your URL Providers were invalid, and have been reset:\r\n\r\n')
for provider in reset_providers:
for provider, (old_prov, new_prov) in reset_providers.items():
# LANG: Text About What Provider Was Reset
popup_text += tr.tl(r'{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n')
popup_text = popup_text.format(
popup_text += tr.tl(r'{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n').format(
PROVIDER=provider,
OLDPROV=reset_providers[provider][0],
NEWPROV=reset_providers[provider][1]
OLDPROV=old_prov,
NEWPROV=new_prov
)
# 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')
popup_text = popup_text.replace('\\n', '\n').replace('\\r', '\r')
tk.messagebox.showinfo(
# LANG: Popup window title for Reset Providers
@ -2243,8 +2228,7 @@ sys.path: {sys.path}'''
detail = tr.tl( # LANG: EDMC Critical Error Details
r"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?"
).format(ERR=err)
detail = detail.replace('\\n', '\n')
detail = detail.replace('\\r', '\r')
detail = detail.replace('\\n', '\n').replace('\\r', '\r')
msg = tk.messagebox.askyesno(
title=title, message=message, detail=detail, icon=tkinter.messagebox.ERROR, type=tkinter.messagebox.YESNO,
parent=root
@ -2275,8 +2259,7 @@ sys.path: {sys.path}'''
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')
popup_text = popup_text.replace('\\n', '\n').replace('\\r', '\r')
tk.messagebox.showinfo(
# LANG: Popup window title for list of 'broken' plugins that failed to load
@ -2306,8 +2289,7 @@ sys.path: {sys.path}'''
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')
popup_text = popup_text.replace('\\n', '\n').replace('\\r', '\r')
tk.messagebox.showinfo(
# LANG: Popup window title for list of 'enabled' plugins that don't work with Python 3.x

View File

@ -153,6 +153,16 @@ See [#1327 - ModuleNotFound when creating a new plugin.](https://github.com/EDCD
for some discussion.
## Common Resources
Some plugins may wish to use resources available in a different plugin, or use
common assets across plugins. This is possible, however care must be taken to
ensure that the plugins do not attempt to load non-existent data or create
circular imports.
For an example of how this is done, look at the code in `plugins/common_coreutils.py`
and the usage of these functions in other core plugins.
---
## Logging

View File

@ -121,6 +121,7 @@ def build() -> None:
"plugins/edsy.py",
"plugins/inara.py",
"plugins/spansh_core.py",
"plugins/common_coreutils.py"
]
options: dict = {
"py2exe": {

61
common_utils.py Normal file
View File

@ -0,0 +1,61 @@
"""
common_utils.py - Common functions and modules.
Copyright (c) EDCD, All Rights Reserved
Licensed under the GNU General Public License.
See LICENSE file.
"""
from __future__ import annotations
import sys
import locale
from typing import TYPE_CHECKING
from EDMCLogging import get_main_logger
if TYPE_CHECKING:
import tkinter as tk
logger = get_main_logger()
SERVER_RETRY = 5 # retry pause for Companion servers [s]
if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import POINT, RECT, SIZE, UINT, BOOL
import win32gui
try:
CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition
CalculatePopupWindowPosition.argtypes = [
ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT)
]
CalculatePopupWindowPosition.restype = BOOL
except Exception: # Not supported under Wine 4.0
CalculatePopupWindowPosition = None # type: ignore
def ensure_on_screen(self, parent: tk.Tk):
"""
Ensure a pop-up window is on the printable screen area.
:param self: The calling class instance of tk.TopLevel
:param parent: The parent window
"""
# Ensure fully on-screen
if sys.platform == 'win32' and CalculatePopupWindowPosition:
position = RECT()
win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()))
if CalculatePopupWindowPosition(
POINT(parent.winfo_rootx(), parent.winfo_rooty()),
SIZE(position.right - position.left, position.bottom - position.top), # type: ignore
0x10000, None, position
):
self.geometry(f"+{position.left}+{position.top}")
def log_locale(prefix: str) -> None:
"""Log all of the current local settings."""
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)}'''
)

190
plugins/common_coreutils.py Normal file
View File

@ -0,0 +1,190 @@
"""
common_coreutils.py - Common Plugin Functions.
Copyright (c) EDCD, All Rights Reserved
Licensed under the GNU General Public License.
See LICENSE file.
This is an EDMC 'core' plugin.
All EDMC plugins are *dynamically* loaded at run-time.
We build for Windows using `py2exe`.
`py2exe` can't possibly know about anything in the dynamically loaded core plugins.
Thus, you **MUST** check if any imports you add in this file are only
referenced in this file (or only in any other core plugin), and if so...
YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN
`build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT
IN AN END-USER INSTALLATION ON WINDOWS.
"""
# pylint: disable=import-error
from __future__ import annotations
from typing import Any, Mapping, cast
import tkinter as tk
import base64
import gzip
import io
import json
import os
import myNotebook as nb # noqa: N813
from EDMCLogging import get_main_logger
from companion import CAPIData
from l10n import translations as tr
logger = get_main_logger()
if not os.getenv('EDMC_NO_UI'): # Functions using show_password_var MUST have TK set up
show_password_var = tk.BooleanVar()
# Global Padding Preferences
PADX = 10
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
PADY = 1 # close spacing
BOXY = 2 # box spacing
SEPY = 10 # seperator line spacing
STATION_UNDOCKED = '×' # "Station" name to display when not docked = U+00D7
def plugin_start3(plugin_dir: str) -> str:
"""
Start the plugin.
:param plugin_dir: NAme of directory this was loaded from.
:return: Identifier string for this plugin.
"""
return 'CommonCoreUtils'
def api_keys_label_common(this, cur_row: int, frame: nb.Frame):
"""
Prepare the box for API Key Loading. This is an EDMC Common Function.
:param this: The module global from the calling module.
:param cur_row: The current row in the calling module's config page.
:param frame: The current frame in the calling module's config page.
:return: The updated module global from the calling module.
"""
# LANG: EDSM API key label
this.apikey_label = nb.Label(frame, text=tr.tl('API Key'))
this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W)
this.apikey = nb.EntryMenu(frame, show="*", width=50)
this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW)
return this
def show_pwd_var_common(frame: nb.Frame, cur_row: int, this):
"""
Allow unmasking of the API Key. This is an EDMC Common Function.
:param cur_row: The current row in the calling module's config page.
:param frame: The current frame in the calling module's config page.
"""
show_password_var.set(False) # Password is initially masked
show_password_checkbox = nb.Checkbutton(
frame,
text=tr.tl('Show API Key'), # LANG: Text EDSM Show API Key
variable=show_password_var,
command=lambda: toggle_password_visibility_common(this)
)
show_password_checkbox.grid(row=cur_row, columnspan=2, padx=BUTTONX, pady=PADY, sticky=tk.W)
# Return a URL for the current ship
def shipyard_url_common(loadout: Mapping[str, Any]) -> str:
"""
Construct a URL for ship loadout. This is an EDMC Common Function.
:param loadout: The ship loadout data.
:return: The constructed URL for the ship loadout.
"""
# Convert loadout to JSON and gzip compress it
string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')
if not string:
return ''
out = io.BytesIO()
with gzip.GzipFile(fileobj=out, mode='w') as f:
f.write(string)
encoded_data = base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D')
return encoded_data
def station_link_common(data: CAPIData, this):
"""
Set the Staion Name. This is an EDMC Common Function.
:param data: A CAPI Data Entry.
:param this: The module global from the calling module.
"""
if data['commander']['docked'] or this.on_foot and this.station_name:
this.station_link['text'] = this.station_name
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
this.station_link['text'] = STATION_UNDOCKED
else:
this.station_link['text'] = ''
def this_format_common(this, state: Mapping[str, Any]):
"""
Gather Common 'This' Elements. This is an EDMC Common Function.
:param this: The module global from the calling module.
:param state: `monitor.state`.
"""
this.system_address = state['SystemAddress']
this.system_name = state['SystemName']
this.system_population = state['SystemPopulation']
this.station_name = state['StationName']
this.station_marketid = state['MarketID']
this.station_type = state['StationType']
this.on_foot = state['OnFoot']
def toggle_password_visibility_common(this):
"""
Toggle if the API Key is visible or not. This is an EDMC Common Function.
:param this: The module global from the calling module.
"""
if show_password_var.get():
this.apikey.config(show="") # type: ignore
else:
this.apikey.config(show="*") # type: ignore
def station_name_setter_common(this):
"""
Set the Station Name. This is an EDMC Common Function.
:param this: The module global from the calling module.
"""
to_set: str = cast(str, this.station_name)
if not to_set:
if this.system_population is not None and this.system_population > 0:
to_set = STATION_UNDOCKED
else:
to_set = ''
this.station_link['text'] = to_set
def cmdr_data_initial_common(this, data: CAPIData):
"""
Set the common CMDR Data. This is an EDMC Common Function.
:param this: The module global from the calling module.
:param data: The latest merged CAPI data.
"""
# Always store initially, even if we're not the *current* system provider.
if not this.station_marketid and data['commander']['docked']:
this.station_marketid = data['lastStarport']['id']
# Only trust CAPI if these aren't yet set
if not this.system_name:
this.system_name = data['lastSystem']['name']
if not this.station_name and data['commander']['docked']:
this.station_name = data['lastStarport']['name']

View File

@ -19,12 +19,10 @@ referenced in this file (or only in any other core plugin), and if so...
`build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT
IN AN END-USER INSTALLATION ON WINDOWS.
"""
# pylint: disable=import-error
from __future__ import annotations
import base64
import gzip
import io
import json
from typing import Any, Mapping
import tkinter as tk
from tkinter import ttk
import myNotebook as nb # noqa: N813 # its not my fault.
@ -32,6 +30,7 @@ from EDMCLogging import get_main_logger
from plug import show_error
from config import config
from l10n import translations as tr
from plugins.common_coreutils import PADX, PADY, BOXY, shipyard_url_common
class CoriolisConfig:
@ -82,10 +81,6 @@ def plugin_start3(path: str) -> str:
def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame:
"""Set up plugin preferences."""
PADX = 10 # noqa: N806
PADY = 1 # noqa: N806
BOXY = 2 # noqa: N806 # box spacing
# Save the old text values for the override mode, so we can update them if the language is changed
coriolis_config.override_text_old_auto = tr.tl('Auto') # LANG: Coriolis normal/beta selection - auto
coriolis_config.override_text_old_normal = tr.tl('Normal') # LANG: Coriolis normal/beta selection - normal
@ -207,14 +202,14 @@ def _get_target_url(is_beta: bool) -> str:
return coriolis_config.normal_url
def shipyard_url(loadout, is_beta) -> str | bool:
"""Return a URL for the current ship."""
# most compact representation
string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')
if not string:
return False
out = io.BytesIO()
with gzip.GzipFile(fileobj=out, mode='w') as f:
f.write(string)
encoded = base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D')
return _get_target_url(is_beta) + encoded
# Return a URL for the current ship
def shipyard_url(loadout: Mapping[str, Any], is_beta: bool) -> bool | str:
"""
Construct a URL for ship loadout.
:param loadout: The ship loadout data.
:param is_beta: Whether the game is in beta.
:return: The constructed URL for the ship loadout.
"""
encoded_data = shipyard_url_common(loadout)
return _get_target_url(is_beta) + encoded_data if encoded_data else False

View File

@ -18,6 +18,7 @@ referenced in this file (or only in any other core plugin), and if so...
`build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT
IN AN END-USER INSTALLATION ON WINDOWS.
"""
# pylint: disable=import-error
from __future__ import annotations
import http
@ -47,6 +48,7 @@ from prefs import prefsVersion
from ttkHyperlinkLabel import HyperlinkLabel
from util import text
from l10n import translations as tr
from plugins.common_coreutils import PADX, PADY, BUTTONX, this_format_common
logger = get_main_logger()
@ -2145,10 +2147,6 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame:
:param is_beta: `bool` - True if this is a beta version of the Game.
:return: The tkinter frame we created.
"""
PADX = 10 # noqa: N806
BUTTONX = 12 # noqa: N806 # indent Checkbuttons and Radiobuttons
PADY = 1 # noqa: N806
if prefsVersion.shouldSetDefaults('0.0.0.0', not bool(config.get_int('output'))):
output: int = config.OUT_EDDN_SEND_STATION_DATA | config.OUT_EDDN_SEND_NON_STATION # default settings
@ -2349,11 +2347,7 @@ def journal_entry( # noqa: C901, CCR001
this.body_id = state['BodyID']
this.body_type = state['BodyType']
this.coordinates = state['StarPos']
this.system_address = state['SystemAddress']
this.system_name = state['SystemName']
this.station_name = state['StationName']
this.station_type = state['StationType']
this.station_marketid = state['MarketID']
this_format_common(this, state)
if event_name == 'docked':
# Trigger a send/retry of pending EDDN messages

View File

@ -18,6 +18,7 @@ referenced in this file (or only in any other core plugin), and if so...
`build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT
IN AN END-USER INSTALLATION ON WINDOWS.
"""
# pylint: disable=import-error
from __future__ import annotations
import json
@ -40,6 +41,9 @@ from edmc_data import DEBUG_WEBSERVER_HOST, DEBUG_WEBSERVER_PORT
from EDMCLogging import get_main_logger
from ttkHyperlinkLabel import HyperlinkLabel
from l10n import translations as tr
from plugins.common_coreutils import (api_keys_label_common, PADX, PADY, BUTTONX, SEPY, BOXY, STATION_UNDOCKED,
show_pwd_var_common, station_link_common, this_format_common,
cmdr_data_initial_common)
# TODO:
@ -118,9 +122,7 @@ class This:
this = This()
show_password_var = tk.BooleanVar()
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
@ -269,14 +271,6 @@ def plugin_stop() -> None:
logger.debug('Done.')
def toggle_password_visibility():
"""Toggle if the API Key is visible or not."""
if show_password_var.get():
this.apikey.config(show="") # type: ignore
else:
this.apikey.config(show="*") # type: ignore
def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame:
"""
Plugin preferences setup hook.
@ -289,12 +283,6 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr
:param is_beta: Whether game beta was detected.
:return: An instance of `myNotebook.Frame`.
"""
PADX = 10 # noqa: N806
BUTTONX = 12 # noqa: N806
PADY = 1 # noqa: N806
BOXY = 2 # noqa: N806
SEPY = 10 # noqa: N806
frame = nb.Frame(parent)
frame.columnconfigure(1, weight=1)
@ -349,23 +337,10 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr
cur_row += 1
# LANG: EDSM API key label
this.apikey_label = nb.Label(frame, text=tr.tl('API Key'))
this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W)
this.apikey = nb.EntryMenu(frame, show="*", width=50)
this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW)
api_keys_label_common(this, cur_row, frame)
cur_row += 1
prefs_cmdr_changed(cmdr, is_beta)
show_password_var.set(False) # Password is initially masked
show_password_checkbox = nb.Checkbutton(
frame,
text=tr.tl('Show API Key'), # LANG: Text EDSM Show API Key
variable=show_password_var,
command=toggle_password_visibility
)
show_password_checkbox.grid(row=cur_row, columnspan=2, padx=BUTTONX, pady=PADY, sticky=tk.W)
show_pwd_var_common(frame, cur_row, this)
return frame
@ -544,11 +519,7 @@ def journal_entry( # noqa: C901, CCR001
this.game_version = state['GameVersion']
this.game_build = state['GameBuild']
this.system_address = state['SystemAddress']
this.system_name = state['SystemName']
this.system_population = state['SystemPopulation']
this.station_name = state['StationName']
this.station_marketid = state['MarketID']
this_format_common(this, state)
entry = new_entry
@ -658,7 +629,7 @@ Queueing: {entry!r}'''
# Update system data
def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001
def cmdr_data(data: CAPIData, is_beta: bool) -> str | None:
"""
Process new CAPI data.
@ -668,14 +639,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001
"""
system = data['lastSystem']['name']
# Always store initially, even if we're not the *current* system provider.
if not this.station_marketid and data['commander']['docked']:
this.station_marketid = data['lastStarport']['id']
# Only trust CAPI if these aren't yet set
if not this.system_name:
this.system_name = data['lastSystem']['name']
if not this.station_name and data['commander']['docked']:
this.station_name = data['lastStarport']['name']
cmdr_data_initial_common(this, data)
# TODO: Fire off the EDSM API call to trigger the callback for the icons
@ -687,13 +651,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001
this.system_link.update_idletasks()
if config.get_str('station_provider') == 'EDSM':
if this.station_link:
if data['commander']['docked'] or this.on_foot and this.station_name:
this.station_link['text'] = this.station_name
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
this.station_link['text'] = STATION_UNDOCKED
else:
this.station_link['text'] = ''
station_link_common(data, this)
# Do *NOT* set 'url' here, as it's set to a function that will call
# through correctly. We don't want a static string.
this.station_link.update_idletasks()

View File

@ -20,11 +20,8 @@ referenced in this file (or only in any other core plugin), and if so...
"""
from __future__ import annotations
import base64
import gzip
import io
import json
from typing import Any, Mapping
from plugins.common_coreutils import shipyard_url_common # pylint: disable=E0401
def plugin_start3(plugin_dir: str) -> str:
@ -46,17 +43,7 @@ def shipyard_url(loadout: Mapping[str, Any], is_beta: bool) -> bool | str:
:param is_beta: Whether the game is in beta.
:return: The constructed URL for the ship loadout.
"""
# Convert loadout to JSON and gzip compress it
string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')
if not string:
return False
out = io.BytesIO()
with gzip.GzipFile(fileobj=out, mode='w') as f:
f.write(string)
encoded_data = shipyard_url_common(loadout)
# Construct the URL using the appropriate base URL based on is_beta
base_url = 'https://edsy.org/beta/#/I=' if is_beta else 'https://edsy.org/#/I='
encoded_data = base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D')
return base_url + encoded_data
return base_url + encoded_data if encoded_data else False

View File

@ -18,6 +18,7 @@ referenced in this file (or only in any other core plugin), and if so...
`build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT
IN AN END-USER INSTALLATION ON WINDOWS.
"""
# pylint: disable=import-error
from __future__ import annotations
import json
@ -43,6 +44,8 @@ from EDMCLogging import get_main_logger
from monitor import monitor
from ttkHyperlinkLabel import HyperlinkLabel
from l10n import translations as tr
from plugins.common_coreutils import (api_keys_label_common, PADX, PADY, BUTTONX, SEPY, station_name_setter_common,
show_pwd_var_common, station_link_common, this_format_common)
logger = get_main_logger()
@ -115,7 +118,7 @@ class This:
self.system_address: str | None = None # type: ignore
self.system_population: int | None = None
self.station_link: tk.Widget = None # type: ignore
self.station = None
self.station_name = None
self.station_marketid = None
# Prefs UI
@ -144,15 +147,12 @@ class This:
this = This()
show_password_var = tk.BooleanVar()
# last time we updated, if unset in config this is 0, which means an instant update
LAST_UPDATE_CONF_KEY = 'inara_last_update'
EVENT_COLLECT_TIME = 31 # Minimum time to take collecting events before requesting a send
WORKER_WAIT_TIME = 35 # Minimum time for worker to wait between sends
STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7
TARGET_URL = 'https://inara.cz/inapi/v1/'
DEBUG = 'inara' in debug_senders
@ -187,9 +187,9 @@ def station_url(system_name: str, station_name: str) -> str:
if system_name and station_name:
return requests.utils.requote_uri(f'https://inara.cz/galaxy-station/?search={system_name}%20[{station_name}]')
if this.system_name and this.station:
if this.system_name and this.station_name:
return requests.utils.requote_uri(
f'https://inara.cz/galaxy-station/?search={this.system_name}%20[{this.station}]')
f'https://inara.cz/galaxy-station/?search={this.system_name}%20[{this.station_name}]')
if system_name:
return system_url(system_name)
@ -233,21 +233,8 @@ def plugin_stop() -> None:
logger.debug('Done.')
def toggle_password_visibility():
"""Toggle if the API Key is visible or not."""
if show_password_var.get():
this.apikey.config(show="")
else:
this.apikey.config(show="*")
def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame:
"""Plugin Preferences UI hook."""
PADX = 10 # noqa: N806
BUTTONX = 12 # noqa: N806 # indent Checkbuttons and Radiobuttons
PADY = 1 # noqa: N806 # close spacing
BOXY = 2 # noqa: N806 # box spacing
SEPY = 10 # noqa: N806 # seperator line spacing
cur_row = 0
frame = nb.Frame(parent)
@ -287,22 +274,10 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame:
cur_row += 1
# LANG: Inara API key label
this.apikey_label = nb.Label(frame, text=tr.tl('API Key')) # Inara setting
this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W)
this.apikey = nb.EntryMenu(frame, show="*", width=50)
this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW)
api_keys_label_common(this, cur_row, frame)
cur_row += 1
prefs_cmdr_changed(cmdr, is_beta)
show_password_var.set(False) # Password is initially masked
show_password_checkbox = nb.Checkbutton(
frame,
text=tr.tl('Show API Key'), # LANG: Text Inara Show API key
variable=show_password_var,
command=toggle_password_visibility,
)
show_password_checkbox.grid(row=cur_row, columnspan=2, padx=BUTTONX, pady=PADY, sticky=tk.W)
show_pwd_var_common(frame, cur_row, this)
return frame
@ -411,15 +386,11 @@ def journal_entry( # noqa: C901, CCR001
# But then we update all the tracking copies before any other checks,
# because they're relevant for URL providing even if *sending* isn't
# appropriate.
this.on_foot = state['OnFoot']
event_name: str = entry['event']
this.cmdr = cmdr
this.FID = state['FID']
this.multicrew = bool(state['Role'])
this.system_name = state['SystemName']
this.system_address = state['SystemAddress']
this.station = state['StationName']
this.station_marketid = state['MarketID']
this_format_common(this, state)
if not monitor.is_live_galaxy():
# Since Update 14 on 2022-11-29 Inara only accepts Live data.
@ -613,7 +584,7 @@ def journal_entry( # noqa: C901, CCR001
elif event_name == 'Undocked':
this.undocked = True
this.station = None
this.station_name = None
elif event_name == 'SupercruiseEntry':
this.undocked = False
@ -1368,14 +1339,7 @@ def journal_entry( # noqa: C901, CCR001
this.system_link.update_idletasks()
if config.get_str('station_provider') == 'Inara':
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
else:
to_set = ''
this.station_link['text'] = to_set
station_name_setter_common(this)
# Do *NOT* set 'url' here, as it's set to a function that will call
# through correctly. We don't want a static string.
this.station_link.update_idletasks()
@ -1383,7 +1347,7 @@ def journal_entry( # noqa: C901, CCR001
return '' # No error
def cmdr_data(data: CAPIData, is_beta): # noqa: CCR001, reanalyze me later
def cmdr_data(data: CAPIData, is_beta):
"""CAPI event hook."""
this.cmdr = data['commander']['name']
@ -1394,8 +1358,8 @@ def cmdr_data(data: CAPIData, is_beta): # noqa: CCR001, reanalyze me later
# Only trust CAPI if these aren't yet set
this.system_name = this.system_name if this.system_name else data['lastSystem']['name']
if not this.station and data['commander']['docked']:
this.station = data['lastStarport']['name']
if not this.station_name and data['commander']['docked']:
this.station_name = data['lastStarport']['name']
# Override standard URL functions
if config.get_str('system_provider') == 'Inara':
@ -1405,14 +1369,7 @@ def cmdr_data(data: CAPIData, is_beta): # noqa: CCR001, reanalyze me later
this.system_link.update_idletasks()
if config.get_str('station_provider') == 'Inara':
if data['commander']['docked'] or this.on_foot and this.station:
this.station_link['text'] = this.station
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
this.station_link['text'] = STATION_UNDOCKED
else:
this.station_link['text'] = ''
station_link_common(data, this)
# Do *NOT* set 'url' here, as it's set to a function that will call
# through correctly. We don't want a static string.

View File

@ -18,14 +18,17 @@ referenced in this file (or only in any other core plugin), and if so...
`build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT
IN AN END-USER INSTALLATION ON WINDOWS.
"""
# pylint: disable=import-error
from __future__ import annotations
import tkinter as tk
from typing import Any, cast
from typing import Any
import requests
from companion import CAPIData
from config import appname, config
from EDMCLogging import get_main_logger
from plugins.common_coreutils import (station_link_common, this_format_common,
cmdr_data_initial_common, station_name_setter_common)
logger = get_main_logger()
@ -47,7 +50,6 @@ class This:
this = This()
STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7
def plugin_start3(plugin_dir: str) -> str:
@ -91,12 +93,7 @@ def journal_entry(
:param state: `monitor.state`
:return: None if no error, else an error string.
"""
this.on_foot = state['OnFoot']
this.system_address = state['SystemAddress']
this.system_name = state['SystemName']
this.system_population = state['SystemPopulation']
this.station_name = state['StationName']
this.station_marketid = state['MarketID']
this_format_common(this, state)
# Only actually change URLs if we are current provider.
if config.get_str('system_provider') == 'spansh':
@ -106,14 +103,7 @@ def journal_entry(
this.system_link.update_idletasks()
if config.get_str('station_provider') == 'spansh':
to_set: str = cast(str, this.station_name)
if not to_set:
if this.system_population is not None and this.system_population > 0:
to_set = STATION_UNDOCKED
else:
to_set = ''
this.station_link['text'] = to_set
station_name_setter_common(this)
# Do *NOT* set 'url' here, as it's set to a function that will call
# through correctly. We don't want a static string.
this.station_link.update_idletasks()
@ -129,15 +119,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None:
:param is_beta: Whether game beta was detected.
:return: Optional error string.
"""
# Always store initially, even if we're not the *current* system provider.
if not this.station_marketid and data['commander']['docked']:
this.station_marketid = data['lastStarport']['id']
# Only trust CAPI if these aren't yet set
if not this.system_name:
this.system_name = data['lastSystem']['name']
if not this.station_name and data['commander']['docked']:
this.station_name = data['lastStarport']['name']
cmdr_data_initial_common(this, data)
# Override standard URL functions
if config.get_str('system_provider') == 'spansh':
@ -146,12 +128,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None:
# through correctly. We don't want a static string.
this.system_link.update_idletasks()
if config.get_str('station_provider') == 'spansh':
if data['commander']['docked'] or this.on_foot and this.station_name:
this.station_link['text'] = this.station_name
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
this.station_link['text'] = STATION_UNDOCKED
else:
this.station_link['text'] = ''
station_link_common(data, this)
# Do *NOT* set 'url' here, as it's set to a function that will call
# through correctly. We don't want a static string.
this.station_link.update_idletasks()

View File

@ -24,6 +24,7 @@ from l10n import translations as tr
from monitor import monitor
from theme import theme
from ttkHyperlinkLabel import HyperlinkLabel
from common_utils import ensure_on_screen
logger = get_main_logger()
@ -187,7 +188,6 @@ if sys.platform == 'win32':
import ctypes
import winreg
from ctypes.wintypes import LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT, BOOL
import win32gui
import win32api
is_wine = False
try:
@ -307,15 +307,7 @@ class PreferencesDialog(tk.Toplevel):
self.grab_set()
# Ensure fully on-screen
if sys.platform == 'win32' and CalculatePopupWindowPosition:
position = RECT()
win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()))
if CalculatePopupWindowPosition(
POINT(parent.winfo_rootx(), parent.winfo_rooty()),
SIZE(position.right - position.left, position.bottom - position.top), # type: ignore
0x10000, None, position
):
self.geometry(f"+{position.left}+{position.top}")
ensure_on_screen(self, parent)
# Set Log Directory
self.logfile_loc = Path(config.app_dir_path / 'logs')
@ -829,7 +821,8 @@ class PreferencesDialog(tk.Toplevel):
appearance_frame,
# LANG: Appearance - Help/hint text for UI scaling selection
text=tr.tl('100 means Default{CR}Restart Required for{CR}changes to take effect!')
).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.E, row=cur_row)
) # E1111
self.ui_scaling_defaultis.grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.E, row=cur_row)
# Transparency slider
ttk.Separator(appearance_frame, orient=tk.HORIZONTAL).grid(
@ -1308,10 +1301,10 @@ class PreferencesDialog(tk.Toplevel):
self._destroy()
# Send to the Post Config if we updated the update branch or need to restart
post_flags = {
'Update': True if self.curr_update_track != self.update_paths.get() else False,
'Update': self.curr_update_track != self.update_paths.get(), # Just needs bool not true if else false
'Track': self.update_paths.get(),
'Parent': self,
'Restart_Req': True if self.req_restart else False
'Restart_Req': self.req_restart # Sipmle Bool Needed
}
if self.callback:
self.callback(**post_flags)

View File

@ -64,9 +64,19 @@ COMMENT_SAME_LINE_RE = re.compile(r"^.*?(#.*)$")
COMMENT_OWN_LINE_RE = re.compile(r"^\s*?(#.*)$")
def extract_comments( # noqa: CCR001
call: ast.Call, lines: list[str], file: pathlib.Path
) -> str | None:
def _extract_lang_comment(line: str, pattern: re.Pattern, file: pathlib.Path,
lineno: int) -> tuple[str | None, str | None]:
"""Attempt to extract a LANG comment from a line using a given regex pattern."""
match = pattern.match(line)
if match:
comment = match.group(1).strip()
if comment.startswith("# LANG:"):
return comment.replace("# LANG:", "").strip(), None
return None, f"Unknown comment for {file}:{lineno} {line}"
return None, None
def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> str | None:
"""
Extract comments from source code based on the given call.
@ -86,29 +96,13 @@ def extract_comments( # noqa: CCR001
above_comment: str | None = None
current_line = lines[current].strip()
current_comment: str | None = None
bad_comment: str | None = None
if above_line is not None:
match = COMMENT_OWN_LINE_RE.match(above_line)
if match:
above_comment = match.group(1).strip()
if not above_comment.startswith("# LANG:"):
bad_comment = f"Unknown comment for {file}:{call.lineno} {above_line}"
above_comment = None
else:
above_comment = above_comment.replace("# LANG:", "").strip()
if above_line:
above_comment, bad_comment = _extract_lang_comment(above_line, COMMENT_OWN_LINE_RE, file, call.lineno)
if current_line is not None:
match = COMMENT_SAME_LINE_RE.match(current_line)
if match:
current_comment = match.group(1).strip()
if not current_comment.startswith("# LANG:"):
bad_comment = f"Unknown comment for {file}:{call.lineno} {current_line}"
current_comment = None
else:
current_comment = current_comment.replace("# LANG:", "").strip()
if current_line:
current_comment, bad_comment = _extract_lang_comment(current_line, COMMENT_SAME_LINE_RE, file, call.lineno)
if current_comment is not None:
out = current_comment

View File

@ -20,25 +20,10 @@ from edmc_data import ship_name_map
from hotkey import hotkeymgr
from l10n import Locale, translations as tr
from monitor import monitor
from common_utils import ensure_on_screen
logger = EDMCLogging.get_main_logger()
if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import POINT, RECT, SIZE, UINT, BOOL
import win32gui
try:
CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition
CalculatePopupWindowPosition.argtypes = [
ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT)
]
CalculatePopupWindowPosition.restype = BOOL
except Exception: # Not supported under Wine 4.0
CalculatePopupWindowPosition = None # type: ignore
CR_LINES_START = 1
CR_LINES_END = 3
RANK_LINES_START = 3
@ -418,16 +403,7 @@ class StatsResults(tk.Toplevel):
self.grab_set()
# Ensure fully on-screen
if sys.platform == 'win32' and CalculatePopupWindowPosition:
position = RECT()
win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()))
if CalculatePopupWindowPosition(
POINT(parent.winfo_rootx(), parent.winfo_rooty()),
# - is evidently supported on the C side
SIZE(position.right - position.left, position.bottom - position.top), # type: ignore
0x10000, None, position
):
self.geometry(f"+{position.left}+{position.top}")
ensure_on_screen(self, parent)
def addpage(
self, parent, header: list[str] | None = None, align: str | None = None