Merge branch 'develop' into develop

This commit is contained in:
David Sangrey authored and GitHub committed 2025-12-05 09:12:56 -05:00
commit d762992609
44 files changed
+277 -178

No files matched your search

+1 -1
View File
@@ -59,7 +59,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
# based on.
- name: Checkout head commits
# https://github.com/actions/checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
#with:
#ref: ${{github.head.sha}}
#repository: ${{github.event.pull_request.head.repo.full_name}}
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
OWNER: 'EDCD'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: '${{ env.CHECKOUT_BRANCH }}'
submodules: true
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
name: Linux environment build steps
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: true
- name: Create .gitversion
@@ -81,7 +81,7 @@ jobs:
shell: powershell
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: true
@@ -158,7 +158,7 @@ jobs:
shell: powershell
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: true
+6 -6
View File
@@ -193,7 +193,7 @@ class Logger:
self.logger_channel_rotating.setFormatter(self.logger_formatter)
self.logger.addHandler(self.logger_channel_rotating)
def get_logger(self) -> 'LoggerMixin':
def get_logger(self) -> LoggerMixin:
"""
Obtain the self.logger of the class instance.
@@ -232,7 +232,7 @@ class Logger:
logger.trace("Not changing log level because it's TRACE") # type: ignore
def get_plugin_logger(plugin_name: str, loglevel: int = _default_loglevel) -> 'LoggerMixin':
def get_plugin_logger(plugin_name: str, loglevel: int = _default_loglevel) -> LoggerMixin:
"""
Return a logger suitable for a plugin.
@@ -352,7 +352,7 @@ class EDMCContextFilter(logging.Filter):
try:
args, _, _, value_dict = inspect.getargvalues(frame)
if len(args) and args[0] in ('self', 'cls'):
frame_class: 'object' = value_dict[args[0]]
frame_class: object = value_dict[args[0]]
if frame_class:
# See https://en.wikipedia.org/wiki/Name_mangling#Python for how name mangling works.
@@ -450,7 +450,7 @@ class EDMCContextFilter(logging.Filter):
# Go up through stack frames until we find the first with a
# type(f_locals.self) of logging.Logger. This should be the start
# of the frames internal to logging.
frame: 'FrameType' = getframe(0)
frame: FrameType = getframe(0)
while frame:
if isinstance(frame.f_locals.get('self'), logging.Logger):
frame = cast('FrameType', frame.f_back) # Want to start on the next frame below
@@ -518,7 +518,7 @@ class EDMCContextFilter(logging.Filter):
return module_name
def get_main_logger(sublogger_name: str = '') -> 'LoggerMixin':
def get_main_logger(sublogger_name: str = '') -> LoggerMixin:
"""Return the correct logger for how the program is being run."""
if not os.getenv("EDMC_NO_UI"):
# GUI app being run
@@ -537,4 +537,4 @@ base_logger_name = appcmdname if os.getenv('EDMC_NO_UI') else appname
edmclogger = Logger(base_logger_name, loglevel=loglevel)
for h in config_logger.handlers[:]:
config_logger.removeHandler(h)
logger: 'LoggerMixin' = edmclogger.get_logger()
logger: LoggerMixin = edmclogger.get_logger()
+6 -6
View File
@@ -54,7 +54,7 @@ if __name__ == '__main__':
log_file_path.mkdir(exist_ok=True)
log_file_path /= f'{appname}.log'
sys.stdout = sys.stderr = open(log_file_path, mode='wt', buffering=1) # Do NOT use WITH here.
sys.stdout = sys.stderr = open(log_file_path, mode='w', buffering=1) # Do NOT use WITH here.
# TODO: Test: Make *sure* this redirect is working, else py2exe is going to cause an exit popup
@@ -216,7 +216,7 @@ if __name__ == '__main__': # noqa: C901
if args.capi_use_debug_access_token:
import config as conf_module
with open(conf_module.config.app_dir_path / 'access_token.txt', 'r') as at:
with open(conf_module.config.app_dir_path / 'access_token.txt') as at:
conf_module.capi_debug_access_token = at.readline().strip()
level_to_set: int | None = None
@@ -476,7 +476,7 @@ class AppWindow:
if sys.platform == 'win32' and not bool(config.get_int('no_systray')):
from simplesystray import SysTrayIcon
def open_window(systray: 'SysTrayIcon', *args) -> None:
def open_window(systray: SysTrayIcon, *args) -> None:
self.w.deiconify()
logfile_loc = pathlib.Path(config.app_dir_path / 'logs')
@@ -612,7 +612,7 @@ class AppWindow:
self.status.grid(columnspan=2, sticky=tk.EW)
for child in frame.winfo_children():
child.grid_configure(padx=self.PADX, pady=(
child.grid_configure(padx=self.PADX, pady=( # type: ignore
sys.platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0)
self.menubar = tk.Menu()
@@ -953,7 +953,7 @@ class AppWindow:
self.cooldown()
def export_market_data(self, data: 'CAPIData') -> bool: # noqa: CCR001
def export_market_data(self, data: CAPIData) -> bool: # noqa: CCR001
"""
Export CAPI market data.
@@ -1872,7 +1872,7 @@ class AppWindow:
h.write(str(companion.session.capi_raw_data).encode(encoding='utf-8'))
if sys.platform == 'win32':
def exit_tray(self, systray: 'SysTrayIcon') -> None:
def exit_tray(self, systray: SysTrayIcon) -> None:
"""Tray icon is shutting down."""
exit_thread = threading.Thread(
target=self.onexit,
+6
View File
@@ -839,3 +839,9 @@
/* edastro_core.py: Settings>EDAstro - Label on checkbox for 'send data'; */
"Send data to EDAstro" = "Send data to EDAstro";
/* common_coreutils.py: Colonisation ship name, with title; In files: common_coreutils.py:81:81 */
"System Colonisation Ship {NAME}" = "System Colonisation Ship {NAME}";
/* common_coreutils.py: Unidentified colonisation ship; In files: common_coreutils.py:84:84 */
"System Colonisation Ship" = "System Colonisation Ship";
+13
View File
@@ -163,6 +163,19 @@ 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.
Plugins may import additional code modules from `load.py` in the usual way. Note
that using an import statement like `import my_module` or `from my_module import
foo` will only import one module with that name **across all plugins**. This can
be used to create a shared library used by several plugins if used carefully,
but may create a conflict if another plugin also has a module of the same name
containing different code. For example if two plugins both contain a
`widgets.py` file and both do `import widgets`, only one of the two modules will
actually be imported.
To avoid this issue, use a relative import statement such as `import .widgets`
or `from .widgets import foo` to ensure that the module is loaded from the
current plugin, even if the name conflicts with another plugin's code.
---
## Logging
+39 -45
View File
@@ -1,9 +1,10 @@
"""Export various CSV formats."""
# -*- coding: utf-8 -*-
import time
import csv
from pathlib import Path
from config import config
from edmc_data import commodity_bracketmap as bracketmap
@@ -12,7 +13,7 @@ from edmc_data import commodity_bracketmap as bracketmap
(COMMODITY_DEFAULT, COMMODITY_CSV) = range(2)
def export(data, kind=COMMODITY_DEFAULT, filename=None) -> None:
def export(data, kind=COMMODITY_DEFAULT, filename=None) -> None: # noqa: CCR001
"""
Export commodity data from the given CAPI data.
@@ -21,55 +22,48 @@ def export(data, kind=COMMODITY_DEFAULT, filename=None) -> None:
:param filename: Filename to write to, or None for a standard format name.
:return:
"""
querytime = config.get_int('querytime', default=int(time.time()))
querytime = config.get_int("querytime", default=int(time.time()))
if not filename:
filename_system = data['lastSystem']['name'].strip()
filename_starport = data['lastStarport']['name'].strip()
filename_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime))
filename_kind = 'csv'
filename = f'{filename_system}.{filename_starport}.{filename_time}.{filename_kind}'
filename = Path(config.get_str('outdir')) / filename
if kind == COMMODITY_CSV:
sep = ';' # BUG: for fixing later after cleanup
header = sep.join(('System', 'Station', 'Commodity', 'Sell', 'Buy', 'Demand', '', 'Supply', '', 'Date', '\n'))
rowheader = sep.join((data['lastSystem']['name'], data['lastStarport']['name']))
else:
sep = ','
header = sep.join(
('System', 'Station', 'Commodity', 'Sell', 'Buy', 'Demand', '', 'Supply', '', 'Average', 'FDevID', 'Date\n')
sysname = data["lastSystem"]["name"].strip()
station = data["lastStarport"]["name"].strip()
timestamp = time.strftime("%Y-%m-%dT%H.%M.%S", time.localtime(querytime))
ext = "csv" if kind == COMMODITY_CSV else "scsv"
filename = (
Path(config.get_str("outdir")) / f"{sysname}.{station}.{timestamp}.{ext}"
)
rowheader = sep.join((data['lastSystem']['name'], data['lastStarport']['name']))
system = data["lastSystem"]["name"]
station = data["lastStarport"]["name"]
with open(filename, 'wt') as h: # codecs can't automatically handle line endings, so encode manually where required
h.write(header)
if kind == COMMODITY_CSV:
header = ["System", "Station", "Commodity", "Sell", "Buy", "Demand",
"", "Supply", "", "Date"]
else:
header = ["System", "Station", "Commodity", "Sell", "Buy", "Demand",
"", "Supply", "", "Average", "FDevID", "Date"]
for commodity in data['lastStarport']['commodities']:
line = sep.join((
rowheader,
commodity['name'],
commodity['sellPrice'] and str(int(commodity['sellPrice'])) or '',
commodity['buyPrice'] and str(int(commodity['buyPrice'])) or '',
str(int(commodity['demand'])) if commodity['demandBracket'] else '',
bracketmap[commodity['demandBracket']],
str(int(commodity['stock'])) if commodity['stockBracket'] else '',
bracketmap[commodity['stockBracket']]
))
with open(filename, "w", newline="", encoding="utf-8") as output_file:
writer = csv.writer(output_file, delimiter=";" if kind == COMMODITY_DEFAULT else ",")
writer.writerow(header)
for commodity in data["lastStarport"]["commodities"]:
row = [
system,
station,
commodity["name"],
int(commodity["sellPrice"]) if commodity.get("sellPrice") is not None else "",
int(commodity["buyPrice"]) if commodity.get("buyPrice") is not None else "",
int(commodity["demand"]) if commodity.get("demandBracket") is not None else "",
bracketmap.get(commodity.get("demandBracket"), ""),
int(commodity["stock"]) if commodity.get("stockBracket") is not None else "",
bracketmap.get(commodity.get("stockBracket"), ""),
]
if kind == COMMODITY_DEFAULT:
line = sep.join(
(
line,
str(int(commodity['meanPrice'])),
str(commodity['id']),
data['timestamp'] + '\n'
)
)
mean = int(commodity["meanPrice"]) if commodity.get("meanPrice") is not None else ""
row.extend([mean, commodity["id"]])
else:
line = sep.join((line, data['timestamp'] + '\n'))
h.write(line)
row.append(data["timestamp"])
writer.writerow(row)
+13 -14
View File
@@ -29,23 +29,22 @@ def ensure_on_screen(self, parent: tk.Tk):
:param self: The calling class instance of tk.TopLevel
:param parent: The parent window
"""
if sys.platform != 'win32':
return
try:
# Get monitor info for the monitor containing the parent window
monitor = win32api.MonitorFromWindow(parent.winfo_id(), win32con.MONITOR_DEFAULTTONEAREST)
monitor_info = win32api.GetMonitorInfo(monitor)
work_area = monitor_info['Work'] # Gets the working area (excludes taskbar)
if sys.platform == 'win32':
try:
# Get monitor info for the monitor containing the parent window
monitor = win32api.MonitorFromWindow(parent.winfo_id(), win32con.MONITOR_DEFAULTTONEAREST)
monitor_info = win32api.GetMonitorInfo(monitor)
work_area = monitor_info['Work'] # Gets the working area (excludes taskbar)
# Calculate optimal position
x = max(work_area[0], min(parent.winfo_rootx(), work_area[2] - self.winfo_width()))
y = max(work_area[1], min(parent.winfo_rooty(), work_area[3] - self.winfo_height()))
# Calculate optimal position
x = max(work_area[0], min(parent.winfo_rootx(), work_area[2] - self.winfo_width()))
y = max(work_area[1], min(parent.winfo_rooty(), work_area[3] - self.winfo_height()))
# Update window position
self.geometry(f"+{x}+{y}")
# Update window position
self.geometry(f"+{x}+{y}")
except Exception as e:
logger.debug(f"Failed to ensure window is on screen: {e}")
except Exception as e:
logger.debug(f"Failed to ensure window is on screen: {e}")
def log_locale(prefix: str) -> None:
+5 -4
View File
@@ -29,7 +29,8 @@ import webbrowser
from email.utils import parsedate
from pathlib import Path
from queue import Queue
from typing import TYPE_CHECKING, Any, Mapping, TypeVar
from typing import TYPE_CHECKING, Any, TypeVar
from collections.abc import Mapping
import requests
import config as conf_module
import killswitch
@@ -69,7 +70,7 @@ class CAPIData(UserDict):
def __init__(
self,
data: str | dict[str, Any] | 'CAPIData' | None = None,
data: str | dict[str, Any] | CAPIData | None = None,
source_host: str | None = None,
source_endpoint: str | None = None,
request_cmdr: str | None = None
@@ -1204,7 +1205,7 @@ def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully
if not (config.app_dir_path / 'FDevIDs' / f).is_file():
logger.warning(f'FDevID file {f} not found! Generating output without these commodity name rewrites.')
continue
with open(config.app_dir_path / 'FDevIDs' / f, 'r') as csvfile:
with open(config.app_dir_path / 'FDevIDs' / f) as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
@@ -1325,7 +1326,7 @@ def index_possibly_sparse_list(data: Mapping[str, V] | list[V], key: int) -> V:
if isinstance(data, list):
return data[key]
if isinstance(data, (dict, dict)):
if isinstance(data, dict):
return data[str(key)]
raise ValueError(f'Unexpected data type {type(data)}')
+3 -2
View File
@@ -43,7 +43,8 @@ import sys
import tomllib
import tomli_w
from time import gmtime
from typing import Any, Callable, Type, TypeVar
from typing import Any, TypeVar
from collections.abc import Callable
import semantic_version
from constants import GITVERSION_FILE, applongname, appname
@@ -474,7 +475,7 @@ class Config:
@staticmethod
def _suppress_call(
func: Callable[..., _T],
exceptions: Type[BaseException] | list[Type[BaseException]] = Exception,
exceptions: type[BaseException] | list[type[BaseException]] = Exception,
*args: Any,
**kwargs: Any,
) -> _T | None:
+2 -2
View File
@@ -12,7 +12,7 @@ import sys
from config import config_logger
if sys.platform != "linux":
raise EnvironmentError("This file is for Linux only.")
raise OSError("This file is for Linux only.")
import pathlib
@@ -137,7 +137,7 @@ class LinuxConfigMinimal:
def close(self) -> None:
"""Release resources (stub)."""
self.config = None
del self.config
def linux_helper(config: Config) -> Config:
+1 -1
View File
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
from . import Config
if sys.platform != "win32":
raise EnvironmentError("This file is for Windows only.")
raise OSError("This file is for Windows only.")
REG_RESERVED_ALWAYS_ZERO = 0
+1 -1
View File
@@ -45,7 +45,7 @@ if __name__ == "__main__": # noqa: C901
# Symbolic name from in-game name
reverse_ship_map = {v: k for k, v in list(ship_name_map.items())}
bulkheads = list(outfitting.armour_map.keys())
bulkheads = list(outfitting.armour_map)
ships = {}
modules = {}
+2 -1
View File
@@ -7,7 +7,8 @@ import pathlib
import threading
import zlib
from http import server
from typing import Any, Callable, Literal
from typing import Any, Literal
from collections.abc import Callable
from urllib.parse import parse_qs
from config import config
from EDMCLogging import get_main_logger
+1 -1
View File
@@ -605,7 +605,7 @@ edmc_suit_symbol_localised = {
# This is sourced from what the game is passed by the launcher, caveat emptor. It was mentioned that / is also
# an option
# This is only run once when this file is imported by something, no runtime cost or repeated expansions will occur
__keys = list(edmc_suit_symbol_localised.keys())
__keys = list(edmc_suit_symbol_localised)
for lang in __keys:
new_lang = lang.replace('\\', r'\\')
new_lang_2 = lang.replace('\\', '/')
+2 -2
View File
@@ -189,7 +189,7 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001
raise
if filename:
with open(filename, 'wt') as h:
with open(filename, 'w') as h:
h.write(string)
return
@@ -206,5 +206,5 @@ def export(data, filename=None) -> None: # noqa: C901, CCR001
timestamp = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(querytime))
filename = pathlib.Path(config.get_str('outdir')) / f'{ship}.{timestamp}.txt'
with open(filename, 'wt') as h:
with open(filename, 'w') as h:
h.write(string)
+1 -1
View File
@@ -6,7 +6,7 @@ from EDMCLogging import get_main_logger
from hotkey import AbstractHotkeyMgr
if sys.platform != 'linux':
raise EnvironmentError("This file is for Linux only.")
raise OSError("This file is for Linux only.")
logger = get_main_logger()
+1 -1
View File
@@ -16,7 +16,7 @@ from EDMCLogging import get_main_logger
from hotkey import AbstractHotkeyMgr
if sys.platform != 'win32':
raise EnvironmentError("This file is for Windows only.")
raise OSError("This file is for Windows only.")
logger = get_main_logger()
+1 -1
View File
@@ -13,7 +13,7 @@ import tkinter as tk
from enum import Enum
from os import getpid as os_getpid
from tkinter import ttk
from typing import Callable
from collections.abc import Callable
from l10n import translations as tr
from config import config
from EDMCLogging import get_main_logger
+3 -2
View File
@@ -11,9 +11,10 @@ import json
import threading
from copy import deepcopy
from typing import (
TYPE_CHECKING, Any, Callable, Mapping, MutableMapping, MutableSequence, NamedTuple, Sequence,
TYPE_CHECKING, Any, NamedTuple,
TypedDict, TypeVar, cast, Union
)
from collections.abc import Callable, Mapping, MutableMapping, MutableSequence, Sequence
import requests
import semantic_version
from semantic_version.base import Version
@@ -217,7 +218,7 @@ class KillSwitchSet:
def __init__(self, kill_switches: list[KillSwitches]) -> None:
self.kill_switches = kill_switches
def get_disabled(self, id: str, *, version: Union[Version, str] = _current_version) -> DisabledResult:
def get_disabled(self, id: str, *, version: Version | str = _current_version) -> DisabledResult:
"""
Return whether the given feature ID is disabled by a killswitch for the given version.
+2 -1
View File
@@ -16,7 +16,8 @@ import re
import sys
from contextlib import suppress
from os import listdir, sep
from typing import Iterable, TextIO, cast
from typing import TextIO, cast
from collections.abc import Iterable
import pathlib
from config import config
from EDMCLogging import get_main_logger
+3 -3
View File
@@ -33,7 +33,7 @@ def export(data: companion.CAPIData, requested_filename: str | None = None) -> N
) # pretty print
if requested_filename is not None and requested_filename:
with open(requested_filename, 'wt') as h:
with open(requested_filename, 'w') as h:
h.write(string)
return
if not requested_filename:
@@ -45,7 +45,7 @@ def export(data: companion.CAPIData, requested_filename: str | None = None) -> N
regexp = re.compile(re.escape(ship) + r'\.\d\d\d\d-\d\d-\d\dT\d\d\.\d\d\.\d\d\.txt')
oldfiles = sorted([x for x in listdir(config.get_str('outdir')) if regexp.match(x)])
if oldfiles:
with open(Path(config.get_str('outdir')) / Path(oldfiles[-1]), 'rU') as h:
with open(Path(config.get_str('outdir')) / Path(oldfiles[-1])) as h:
if h.read() == string:
return # same as last time - don't write
@@ -57,5 +57,5 @@ def export(data: companion.CAPIData, requested_filename: str | None = None) -> N
ship_time = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime(query_time))
file_path = output_directory / f"{ship}.{ship_time}.txt"
with open(file_path, 'wt') as h:
with open(file_path, 'w') as h:
h.write(string)
+16 -7
View File
@@ -18,7 +18,8 @@ from collections import defaultdict
from os import SEEK_END, SEEK_SET, listdir
from os.path import basename, expanduser, getctime, isdir, join
from time import gmtime, localtime, mktime, sleep, strftime, strptime, time
from typing import TYPE_CHECKING, Any, BinaryIO, MutableMapping
from typing import TYPE_CHECKING, Any, BinaryIO
from collections.abc import MutableMapping
import psutil
import semantic_version
import util_ships
@@ -347,7 +348,7 @@ class EDLogs(FileSystemEventHandler):
"""
return bool(self.thread and self.thread.is_alive())
def on_created(self, event: 'FileSystemEvent') -> None:
def on_created(self, event: FileSystemEvent) -> None:
"""Watchdog callback when, e.g. client (re)started."""
if not event.is_directory and self._RE_LOGFILE.search(str(basename(event.src_path))):
@@ -811,6 +812,10 @@ class EDLogs(FileSystemEventHandler):
self.state['OnFoot'] = False
self.state['Taxi'] = entry['Taxi']
if entry['Multicrew']:
# Player has boarded another player's ship, but has not selected a role yet
self.state['Role'] = 'Idle'
# We can't now have anything in the BackPack, it's all in the
# ShipLocker.
self.backpack_set_empty()
@@ -846,6 +851,10 @@ class EDLogs(FileSystemEventHandler):
self.state['Taxi'] = False
self.state['Dropship'] = False
# Since the player is now on foot, it's no longer possible to be multicrewing, so no
# need to actually check the multicrew parameter.
self.state['Role'] = None
elif event_type == 'dropshipdeploy':
# We're definitely on-foot now
self.state['OnFoot'] = True
@@ -2267,7 +2276,7 @@ class EDLogs(FileSystemEventHandler):
string = json.dumps(self.ship(False), ensure_ascii=False, indent=2, separators=(',', ': ')) # pretty print
if filename:
try:
with open(filename, 'wt', encoding='utf-8') as h:
with open(filename, 'w', encoding='utf-8') as h:
h.write(string)
except UnicodeError:
@@ -2276,7 +2285,7 @@ class EDLogs(FileSystemEventHandler):
)
try:
with open(filename, 'wt') as h:
with open(filename, 'w') as h:
h.write(string)
except OSError:
@@ -2291,7 +2300,7 @@ class EDLogs(FileSystemEventHandler):
ship = util_ships.ship_file_name(self.state['ShipName'], self.state['ShipType'])
regexp = re.compile(re.escape(ship) + r'\.\d{4}-\d\d-\d\dT\d\d\.\d\d\.\d\d\.txt')
oldfiles = sorted((x for x in listdir(config.get_str('outdir')) if regexp.match(x)))
oldfiles = sorted(x for x in listdir(config.get_str('outdir')) if regexp.match(x))
if oldfiles:
try:
with open(join(config.get_str('outdir'), oldfiles[-1]), encoding='utf-8') as h:
@@ -2323,13 +2332,13 @@ class EDLogs(FileSystemEventHandler):
filename = join(config.get_str('outdir'), f'{ship}.{ts}.txt')
try:
with open(filename, 'wt', encoding='utf-8') as h:
with open(filename, 'w', encoding='utf-8') as h:
h.write(string)
except UnicodeError:
logger.exception("UnicodeError writing ship loadout to new filename with utf-8 encoding, trying without...")
try:
with open(filename, 'wt') as h:
with open(filename, 'w') as h:
h.write(string)
except OSError:
+16 -4
View File
@@ -14,6 +14,7 @@ import tkinter as tk
from tkinter import ttk, messagebox
from PIL import ImageGrab
from l10n import translations as tr
from collections.abc import Callable
if sys.platform == 'win32':
PAGEFG = 'SystemWindowText'
@@ -50,10 +51,18 @@ class Label(ttk.Label):
"""Custom ttk.Label class to fix some display issues."""
def __init__(self, master: ttk.Frame | None = None, **kw):
kw['foreground'] = kw.pop('foreground', PAGEFG if sys.platform == 'win32'
else ttk.Style().lookup('TLabel', 'foreground'))
kw['background'] = kw.pop('background', PAGEBG if sys.platform == 'win32'
else ttk.Style().lookup('TLabel', 'background'))
if sys.platform == 'win32':
fg_color = PAGEFG
else:
fg_color = ttk.Style().lookup('TLabel', 'foreground')
if sys.platform == 'win32':
bg_color = PAGEBG
else:
bg_color = ttk.Style().lookup('TLabel', 'background')
kw['foreground'] = kw.pop('foreground', fg_color)
kw['background'] = kw.pop('background', bg_color)
super().__init__(master, **kw)
@@ -171,6 +180,7 @@ class ScrollableNotebook(Notebook):
self,
master: ttk.Frame | None = None,
tabmenu: bool = False,
on_tab_change: Callable | None = None,
*args,
**kwargs
) -> None:
@@ -191,6 +201,8 @@ class ScrollableNotebook(Notebook):
self.notebookTab: ttk.Notebook = ttk.Notebook(self, **kwargs)
self.notebookTab.place(x=0, y=0)
self.notebookTab.bind("<<NotebookTabChanged>>", self._tab_changer)
if on_tab_change:
self.notebookTab.bind("<<NotebookTabChanged>>", on_tab_change, add='+')
# Sliding frame and controls
slide_frame: ttk.Frame = ttk.Frame(self)
+1 -1
View File
@@ -296,7 +296,7 @@ def export(data, filename) -> None:
header = 'System,Station,Category,Name,Mount,Guidance,Ship,Class,Rating,FDevID,Date\n'
rowheader = f'{data["lastSystem"]["name"]},{data["lastStarport"]["name"]}'
with open(filename, 'wt') as h:
with open(filename, 'w') as h:
h.write(header)
for v in data["lastStarport"].get("modules", {}).values():
try:
+24 -11
View File
@@ -18,7 +18,8 @@ import tkinter as tk
from datetime import datetime, timedelta, timezone
from pathlib import Path
from tkinter import ttk
from typing import Any, Mapping, MutableMapping
from typing import Any
from collections.abc import Mapping, MutableMapping
import companion
import myNotebook as nb # noqa: N813
@@ -48,13 +49,20 @@ last_error = LastError()
class Plugin:
"""An EDMC plugin."""
def __init__(self, name: str, loadfile: Path | None, plugin_logger: logging.Logger | None): # noqa: CCR001
def __init__( # noqa: CCR001
self,
name: str,
loadfile: Path | None,
plugin_logger: logging.Logger | None,
internal: bool = False
):
"""
Load a single plugin.
:param name: Base name of the file being loaded from.
:param loadfile: Full path/filename of the plugin.
:param plugin_logger: The logging instance for this plugin to use.
:param internal: True to use internal plugin loader logic. Defaults to False.
:raises Exception: Typically, ImportError or OSError
"""
self.name: str = name # Display name.
@@ -65,14 +73,19 @@ class Plugin:
if loadfile:
logger.info(f'loading plugin "{name.replace(".", "_")}" from "{loadfile}"')
try:
filename = 'plugin_'
filename += name.encode(encoding='ascii', errors='replace').decode('utf-8').replace('.', '_')
spec = importlib.util.spec_from_file_location(filename, loadfile)
# Replaces older load_module() code. Includes a safety check that the module name is set.
if spec is not None and spec.loader is not None:
module = importlib.util.module_from_spec(spec)
sys.modules[module.__name__] = module
spec.loader.exec_module(module)
if internal:
filename = 'plugin_'
filename += name.encode(encoding='ascii', errors='replace').decode('utf-8').replace('.', '_')
spec = importlib.util.spec_from_file_location(filename, loadfile)
# Replaces older load_module() code. Includes a safety check that the module name is set.
if spec is not None and spec.loader is not None:
module = importlib.util.module_from_spec(spec)
sys.modules[module.__name__] = module
spec.loader.exec_module(module)
else:
module = importlib.import_module('.load', name)
if module:
if getattr(module, 'plugin_start3', None):
newname = module.plugin_start3(Path(loadfile).resolve().parent)
self.name = str(newname) if newname else self.name
@@ -170,7 +183,7 @@ def _load_internal_plugins():
try:
plugin_name = name[:-3]
plugin_path = config.internal_plugin_dir_path / name
plugin = Plugin(plugin_name, plugin_path, logger)
plugin = Plugin(plugin_name, plugin_path, logger, internal=True)
plugin.folder = None
internal.append(plugin)
except Exception:
+49 -2
View File
@@ -22,13 +22,15 @@ referenced in this file (or only in any other core plugin), and if so...
# pylint: disable=import-error
from __future__ import annotations
from typing import Any, Mapping, cast
from typing import Any, cast
from collections.abc import Mapping
import tkinter as tk
import base64
import gzip
import io
import json
import os
import re
import myNotebook as nb # noqa: N813
from EDMCLogging import get_main_logger
from companion import CAPIData
@@ -47,6 +49,49 @@ SEPY = 10 # seperator line spacing
STATION_UNDOCKED = '×' # "Station" name to display when not docked = U+00D7
# Known colonisation ship names match one of the following:
# "$EXT_PANEL_ColonisationShip;"
# "$EXT_PANEL_ColonisationShip; <ship name>" with "<ship name>" being eg; "Okawara Horizons"
# "$EXT_PANEL_ColonisationShip:#index=<number>;"
# "$EXT_PANEL_ColonisationShip:#index=<number>; <ship name>"
_RE_COLONISATION_SHIP = re.compile(r'\$EXT_PANEL_ColonisationShip(?::#index=(\d+))?;(?: (.+))?')
def localised_name(value: str | None) -> str | None:
"""
Handle localisation of strings in places like station names.
This is currently an issue for system colonisation ships that have a station name along the
lines of "$EXT_PANEL_ColonisationShip; Okawara Horizons" when written to the journal, but would
be better displayed as "System Colonisation Ship Okawara Horizona" (or other appropriately
localised equivalent).
If no (known) localisable strings are present, the input will be returned unmodified.
:param value: The non-localised string.
:return: The localised string, or None if the input was None.
"""
if value is None:
return None
colonisation_ship_match = _RE_COLONISATION_SHIP.match(value)
if colonisation_ship_match:
ship_index = colonisation_ship_match.group(1)
ship_name = colonisation_ship_match.group(2)
# Use the ship's index as its name if no name was provided.
ship_id = ship_name or ship_index
if ship_id:
# LANG: Colonisation ship name, with title
value = tr.tl("System Colonisation Ship {NAME}").format(NAME=ship_id)
else:
# LANG: Unidentified colonisation ship
value = tr.tl("System Colonisation Ship")
return value
def plugin_start3(plugin_dir: str) -> str:
"""
Start the plugin.
@@ -121,7 +166,7 @@ def station_link_common(data: CAPIData, this):
: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
this.station_link['text'] = localised_name(this.station_name)
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
this.station_link['text'] = STATION_UNDOCKED
else:
@@ -163,12 +208,14 @@ def station_name_setter_common(this):
: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 = ''
to_set = str(localised_name(to_set))
this.station_link['text'] = to_set
+2 -1
View File
@@ -22,7 +22,8 @@ referenced in this file (or only in any other core plugin), and if so...
# pylint: disable=import-error
from __future__ import annotations
from typing import Any, Mapping
from typing import Any
from collections.abc import Mapping
import tkinter as tk
from tkinter import ttk
import myNotebook as nb # noqa: N813 # its not my fault.
+3 -2
View File
@@ -33,7 +33,8 @@ from tkinter import ttk
from platform import system
from textwrap import dedent
from threading import Lock
from typing import Any, Iterator, Mapping, MutableMapping
from typing import Any
from collections.abc import Iterator, Mapping, MutableMapping
import requests
import companion
import edmc_data
@@ -152,7 +153,7 @@ class EDDNSender:
r"unable to validate.',\)]$"
)
def __init__(self, eddn: 'EDDN', eddn_endpoint: str) -> None:
def __init__(self, eddn: EDDN, eddn_endpoint: str) -> None:
"""
Prepare the system for processing messages.
+4 -3
View File
@@ -29,7 +29,8 @@ from queue import Queue
from threading import Thread
from time import sleep
from tkinter import ttk
from typing import Any, Literal, Mapping, MutableMapping, cast, Sequence
from typing import Any, Literal, cast
from collections.abc import Mapping, MutableMapping, Sequence
import requests
import killswitch
import monitor
@@ -43,7 +44,7 @@ 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)
cmdr_data_initial_common, localised_name)
# TODO:
@@ -543,7 +544,7 @@ entry: {entry!r}'''
to_set = ''
if this.station_link:
this.station_link['text'] = to_set
this.station_link['text'] = localised_name(to_set)
this.station_link['url'] = station_url(str(this.system_name), str(this.station_name))
this.station_link.update_idletasks()
+2 -1
View File
@@ -20,7 +20,8 @@ referenced in this file (or only in any other core plugin), and if so...
"""
from __future__ import annotations
from typing import Any, Mapping
from typing import Any
from collections.abc import Mapping
from plugins.common_coreutils import shipyard_url_common # pylint: disable=E0401
+4 -13
View File
@@ -31,7 +31,8 @@ from datetime import datetime, timedelta, timezone
from operator import itemgetter
from threading import Lock, Thread
from tkinter import ttk
from typing import Any, Callable, Deque, Mapping, NamedTuple, Sequence, cast, Union
from typing import Any, Deque, NamedTuple, cast, Union
from collections.abc import Callable, Mapping, Sequence
import requests
import edmc_data
import killswitch
@@ -105,7 +106,6 @@ class This:
self.materials: list[dict[str, Any]] | None = None
self.last_credits: int = 0 # Send credit update soon after Startup / new game
self.storedmodules: list[dict[str, Any]] | None = None
self.embarked: bool = False # Boarded Someone Else's Ship? Should always start as False.
self.loadout: dict[str, Any] | None = None
self.fleet: list[dict[str, Any]] | None = None
self.shipswap: bool = False # just swapped ship
@@ -533,17 +533,8 @@ def journal_entry( # noqa: C901, CCR001
power_data = {'powerName': entry["Power"], 'rankValue': entry["Rank"]}
new_add_event('setCommanderRankPower', entry['timestamp'], power_data)
# Handle Embark/Disembark Events
if event_name == 'Embark':
this.embarked = True
this.multicrew = True
if event_name == 'Disembark':
this.embarked = False
this.multicrew = False
# Ship change
if event_name == 'Loadout' and this.shipswap:
elif event_name == 'Loadout' and this.shipswap:
this.loadout = make_loadout(state)
new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
this.shipswap = False
@@ -831,7 +822,7 @@ def journal_entry( # noqa: C901, CCR001
for ship in this.fleet:
new_add_event('setCommanderShip', entry['timestamp'], ship)
# Loadout
if event_name == 'Loadout' and not this.embarked: # Ignore Loadout Events if Embarked
if event_name == 'Loadout':
loadout = make_loadout(state)
if this.loadout != loadout:
this.loadout = loadout
+16 -12
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""EDMC preferences library."""
from __future__ import annotations
@@ -14,7 +13,8 @@ from os import system
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
from tkinter import ttk
from types import TracebackType
from typing import Any, Callable, Optional, Type
from typing import Any
from collections.abc import Callable
import myNotebook as nb # noqa: N813
import plug
from config import appversion_nobuild, config
@@ -170,8 +170,8 @@ class AutoInc(contextlib.AbstractContextManager):
def __exit__(
self,
exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]
) -> Optional[bool]:
exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
) -> bool | None:
"""Do nothing."""
return None
@@ -241,7 +241,7 @@ if sys.platform == 'win32':
class PreferencesDialog(tk.Toplevel):
"""The EDMC preferences dialog."""
def __init__(self, parent: tk.Tk, callback: Optional[Callable]):
def __init__(self, parent: tk.Tk, callback: Callable | None):
super().__init__(parent)
self.parent = parent
@@ -265,7 +265,7 @@ class PreferencesDialog(tk.Toplevel):
self.cmdr: str | bool | None = False # Note if Cmdr changes in the Journal
self.is_beta: bool = False # Note if Beta status changes in the Journal
self.cmdrchanged_alarm: Optional[str] = None # This stores an ID that can be used to cancel a scheduled call
self.cmdrchanged_alarm: str | None = None # This stores an ID that can be used to cancel a scheduled call
# Set up the main frame
frame = ttk.Frame(self)
@@ -276,8 +276,7 @@ class PreferencesDialog(tk.Toplevel):
frame.rowconfigure(0, weight=1)
frame.rowconfigure(1, weight=0)
notebook: nb.ScrollableNotebook = nb.ScrollableNotebook(frame, tabmenu=True)
notebook.bind('<<NotebookTabChanged>>', self.tabchanged) # Recompute on tab change
notebook: nb.ScrollableNotebook = nb.ScrollableNotebook(frame, tabmenu=True, on_tab_change=self.tabchanged)
self.PADX = 10
self.BUTTONX = 12 # indent Checkbuttons and Radiobuttons
@@ -310,6 +309,11 @@ class PreferencesDialog(tk.Toplevel):
self.cmdrchanged()
self.themevarchanged()
# Init hotkey attributes to avoid linter errors
self.hotkey_text: ttk.Entry
self.hotkey_only_btn: nb.Checkbutton
self.hotkey_play_btn: nb.Checkbutton
# disable hotkey for the duration
hotkeymgr.unregister()
@@ -1132,7 +1136,7 @@ class PreferencesDialog(tk.Toplevel):
"""Handle preferences active tab changing."""
self.outvarchanged()
def outvarchanged(self, event: Optional[tk.Event] = None) -> None:
def outvarchanged(self, event: tk.Event | None = None) -> None:
"""Handle Output tab variable changes."""
self.displaypath(self.outdir, self.outdir_entry)
self.displaypath(self.logdir, self.logdir_entry)
@@ -1236,14 +1240,14 @@ class PreferencesDialog(tk.Toplevel):
self.theme_button_0['state'] = state
self.theme_button_1['state'] = state
def hotkeystart(self, event: 'tk.Event[Any]') -> None:
def hotkeystart(self, event: tk.Event[Any]) -> None:
"""Start listening for hotkeys."""
event.widget.bind('<KeyPress>', self.hotkeylisten)
event.widget.bind('<KeyRelease>', self.hotkeylisten)
event.widget.delete(0, tk.END)
hotkeymgr.acquire_start()
def hotkeyend(self, event: 'tk.Event[Any]') -> None:
def hotkeyend(self, event: tk.Event[Any]) -> None:
"""Stop listening for hotkeys."""
event.widget.unbind('<KeyPress>')
event.widget.unbind('<KeyRelease>')
@@ -1254,7 +1258,7 @@ class PreferencesDialog(tk.Toplevel):
# LANG: No hotkey/shortcut set
hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else tr.tl('None'))
def hotkeylisten(self, event: 'tk.Event[Any]') -> str:
def hotkeylisten(self, event: tk.Event[Any]) -> str:
"""
Hotkey handler.
+3 -3
View File
@@ -12,7 +12,7 @@ import os
import sys
import threading
from urllib import parse
from typing import TYPE_CHECKING, Type
from typing import TYPE_CHECKING
from config import config
from constants import appname, protocolhandler_redirect
from EDMCLogging import get_main_logger
@@ -61,7 +61,7 @@ if config.auth_force_edmc_protocol or ( # noqa: C901
):
# This could be false if you use auth_force_edmc_protocol, but then you get to keep the pieces
if sys.platform != "win32":
raise EnvironmentError("This code is for Windows only.")
raise OSError("This code is for Windows only.")
# spell-checker: words HBRUSH HICON WPARAM wstring WNDCLASS HMENU HGLOBAL
from ctypes import ( # type: ignore
windll,
@@ -434,7 +434,7 @@ else: # Linux / Run from source
)
def get_handler_impl() -> Type[GenericProtocolHandler]:
def get_handler_impl() -> type[GenericProtocolHandler]:
"""
Get the appropriate GenericProtocolHandler for the current system and config.
+4 -4
View File
@@ -13,7 +13,7 @@ flake8-json==24.4.0
flake8-noqa==1.4.0
flake8-use-fstring==1.4
mypy==1.18.2
mypy==1.19.0
pep8-naming==0.15.1
safety==3.7.0
types-requests==2.32.4.20250913
@@ -23,7 +23,7 @@ types-setuptools==80.9.0.20250822
autopep8==2.3.2
# Git pre-commit checking
pre-commit==4.4.0
pre-commit==4.5.0
# HTML changelogs
mistune==3.1.4
@@ -33,9 +33,9 @@ mistune==3.1.4
py2exe==0.14.0.0; sys_platform == 'win32'
# Testing
pytest==9.0.0
pytest==9.0.1
pytest-cov==7.0.0 # Pytest code coverage support
coverage[toml]==7.11.2 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
coverage[toml]==7.12.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
coverage-conditional-plugin==0.9.0
+1 -1
View File
@@ -188,7 +188,7 @@ class FileLocation:
line_end_col: int | None
@staticmethod
def from_call(path: pathlib.Path, c: ast.Call) -> "FileLocation":
def from_call(path: pathlib.Path, c: ast.Call) -> FileLocation:
"""
Create a FileLocation from a Call and Path.
+2 -1
View File
@@ -12,7 +12,8 @@ import json
import sys
import tkinter as tk
from tkinter import ttk
from typing import Any, AnyStr, Callable, NamedTuple, Sequence, cast
from typing import Any, AnyStr, NamedTuple, cast
from collections.abc import Callable, Sequence
import companion
import EDMCLogging
import myNotebook as nb # noqa: N813
+6 -6
View File
@@ -30,18 +30,18 @@ def export(data: CAPIData) -> None:
# required
with open(data_path / data_filename, 'wb') as h:
# Format described here: https://github.com/eyeonus/Trade-Dangerous/wiki/Price-Data
h.write('#! trade.py import -\n'.encode('utf-8'))
h.write(b'#! trade.py import -\n')
this_platform = system()
cmdr_name = data['commander']['name'].strip()
h.write(
f'# Created by {applongname} {appversion()} on {this_platform} for Cmdr {cmdr_name}.\n'.encode('utf-8')
f'# Created by {applongname} {appversion()} on {this_platform} for Cmdr {cmdr_name}.\n'.encode()
)
h.write(
'#\n# <item name> <sellCR> <buyCR> <demand> <stock> <timestamp>\n\n'.encode('utf-8')
b'#\n# <item name> <sellCR> <buyCR> <demand> <stock> <timestamp>\n\n'
)
system_name = data['lastSystem']['name'].strip()
starport_name = data['lastStarport']['name'].strip()
h.write(f'@ {system_name}/{starport_name}\n'.encode('utf-8'))
h.write(f'@ {system_name}/{starport_name}\n'.encode())
# sort commodities by category
by_category = defaultdict(list)
@@ -50,7 +50,7 @@ def export(data: CAPIData) -> None:
timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.strptime(data['timestamp'], '%Y-%m-%dT%H:%M:%SZ'))
for category in sorted(by_category):
h.write(f' + {format(category)}\n'.encode('utf-8'))
h.write(f' + {format(category)}\n'.encode())
# corrections to commodity names can change the sort order
for commodity in sorted(by_category[category], key=itemgetter('name')):
h.write(
@@ -61,5 +61,5 @@ def export(data: CAPIData) -> None:
f"{demandbracketmap[commodity['demandBracket']]:1}"
f" {int(commodity['stock']) if commodity['stockBracket'] else '':8}"
f"{stockbracketmap[commodity['stockBracket']]:1}"
f" {timestamp}\n".encode('utf-8')
f" {timestamp}\n".encode()
)
+1 -1
View File
@@ -5,7 +5,7 @@ import multiprocessing as mp
import os
import pathlib
import sys
from typing import Generator
from collections.abc import Generator
import pytest
from pytest import MonkeyPatch, TempdirFactory, TempPathFactory
from config import config
+3 -3
View File
@@ -15,7 +15,7 @@ import sys
import tkinter as tk
from tkinter import font as tk_font
from tkinter import ttk
from typing import Callable
from collections.abc import Callable
from l10n import translations as tr
from config import config
from EDMCLogging import get_main_logger
@@ -208,7 +208,7 @@ class _Theme:
if isinstance(widget, (tk.Frame, ttk.Frame)):
for child in widget.winfo_children():
self.register(child)
self.register(child) # type: ignore
def register_alternate(self, pair: tuple, gridopts: dict) -> None:
self.widgets_pair.append((pair, gridopts))
@@ -313,7 +313,7 @@ class _Theme:
self._update_widget(widget)
if isinstance(widget, (tk.Frame, ttk.Frame)):
for child in widget.winfo_children():
self._update_widget(child)
self._update_widget(child) # type: ignore
# Apply current theme to a single widget
def _update_widget(self, widget: tk.Widget | tk.BitmapImage) -> None: # noqa: CCR001, C901
+2 -1
View File
@@ -6,6 +6,7 @@ Licensed under the GNU General Public License.
See LICENSE file.
"""
import os
import sys
from pathlib import Path
from edmc_data import ship_name_map
@@ -18,7 +19,7 @@ def ship_file_name(ship_name: str, ship_type: str) -> str:
name = Path(name).with_suffix("").name
# Check if the name is a reserved filename
if os.path.isreserved(name):
if sys.platform == 'win32' and os.path.isreserved(name):
name += "_"
return name.translate(