diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a9e5e523..e9044573 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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 diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index d49d970d..dd9ee099 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -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}} diff --git a/.github/workflows/push-checks.yml b/.github/workflows/push-checks.yml index 51c9d47b..f6a1b0ef 100644 --- a/.github/workflows/push-checks.yml +++ b/.github/workflows/push-checks.yml @@ -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 diff --git a/.github/workflows/submodule-update.yml b/.github/workflows/submodule-update.yml index 5dbba221..e74e1020 100644 --- a/.github/workflows/submodule-update.yml +++ b/.github/workflows/submodule-update.yml @@ -24,7 +24,7 @@ jobs: OWNER: 'EDCD' steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: ref: '${{ env.CHECKOUT_BRANCH }}' submodules: true diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 8418c2dd..79cc024f 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -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 diff --git a/EDMCLogging.py b/EDMCLogging.py index 201648a2..a05ad107 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -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() diff --git a/EDMarketConnector.py b/EDMarketConnector.py index e7bdf490..762afd41 100644 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -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, diff --git a/L10n/en.template b/L10n/en.template index faac3259..f670e39c 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -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"; diff --git a/PLUGINS.md b/PLUGINS.md index 6fd20ef9..71aaec7a 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -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 diff --git a/commodity.py b/commodity.py index 52c0f8b8..52c13928 100644 --- a/commodity.py +++ b/commodity.py @@ -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) diff --git a/common_utils.py b/common_utils.py index a6f78356..ce3c7986 100644 --- a/common_utils.py +++ b/common_utils.py @@ -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: diff --git a/companion.py b/companion.py index 19f12a14..2c920144 100644 --- a/companion.py +++ b/companion.py @@ -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)}') diff --git a/config/__init__.py b/config/__init__.py index 83d73d71..94d84c14 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -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: diff --git a/config/linux.py b/config/linux.py index 000946be..319e7e87 100644 --- a/config/linux.py +++ b/config/linux.py @@ -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: diff --git a/config/windows.py b/config/windows.py index ee654c4b..6868493e 100644 --- a/config/windows.py +++ b/config/windows.py @@ -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 diff --git a/coriolis-update-files.py b/coriolis-update-files.py index 40d25fb3..f15d4067 100755 --- a/coriolis-update-files.py +++ b/coriolis-update-files.py @@ -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 = {} diff --git a/debug_webserver.py b/debug_webserver.py index c4be2be3..545e62bd 100644 --- a/debug_webserver.py +++ b/debug_webserver.py @@ -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 diff --git a/edmc_data.py b/edmc_data.py index f29795f4..1a94b3ab 100644 --- a/edmc_data.py +++ b/edmc_data.py @@ -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('\\', '/') diff --git a/edshipyard.py b/edshipyard.py index f7bceb75..e1c14e23 100644 --- a/edshipyard.py +++ b/edshipyard.py @@ -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) diff --git a/hotkey/linux.py b/hotkey/linux.py index 7e4b9862..33e9cd7e 100644 --- a/hotkey/linux.py +++ b/hotkey/linux.py @@ -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() diff --git a/hotkey/windows.py b/hotkey/windows.py index b292e7a7..9d32edaa 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -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() diff --git a/journal_lock.py b/journal_lock.py index 3e16b309..8d90314e 100644 --- a/journal_lock.py +++ b/journal_lock.py @@ -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 diff --git a/killswitch.py b/killswitch.py index 22a40049..bec23ba8 100644 --- a/killswitch.py +++ b/killswitch.py @@ -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. diff --git a/l10n.py b/l10n.py index 57c8651b..48197592 100755 --- a/l10n.py +++ b/l10n.py @@ -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 diff --git a/loadout.py b/loadout.py index 144439c0..c465cdbd 100644 --- a/loadout.py +++ b/loadout.py @@ -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) diff --git a/monitor.py b/monitor.py index 3df0f85c..bc1d44f0 100644 --- a/monitor.py +++ b/monitor.py @@ -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: diff --git a/myNotebook.py b/myNotebook.py index 46f79622..c28d7873 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -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("<>", self._tab_changer) + if on_tab_change: + self.notebookTab.bind("<>", on_tab_change, add='+') # Sliding frame and controls slide_frame: ttk.Frame = ttk.Frame(self) diff --git a/outfitting.py b/outfitting.py index 4b014e48..3256da31 100644 --- a/outfitting.py +++ b/outfitting.py @@ -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: diff --git a/plug.py b/plug.py index 1a8779f7..77d73f9e 100644 --- a/plug.py +++ b/plug.py @@ -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: diff --git a/plugins/common_coreutils.py b/plugins/common_coreutils.py index 6b90d4dc..2e3b58ae 100644 --- a/plugins/common_coreutils.py +++ b/plugins/common_coreutils.py @@ -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; " with "" being eg; "Okawara Horizons" +# "$EXT_PANEL_ColonisationShip:#index=;" +# "$EXT_PANEL_ColonisationShip:#index=; " +_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 diff --git a/plugins/coriolis.py b/plugins/coriolis.py index 89f128f7..be0bd4be 100644 --- a/plugins/coriolis.py +++ b/plugins/coriolis.py @@ -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. diff --git a/plugins/eddn.py b/plugins/eddn.py index 6410d0b2..8b783c8f 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -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. diff --git a/plugins/edsm.py b/plugins/edsm.py index e1e6b168..46c8ef4e 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -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() diff --git a/plugins/edsy.py b/plugins/edsy.py index b1bdbaf1..109bb2f0 100644 --- a/plugins/edsy.py +++ b/plugins/edsy.py @@ -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 diff --git a/plugins/inara.py b/plugins/inara.py index 56df1e0e..be3f517b 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -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 diff --git a/prefs.py b/prefs.py index 71e03087..9b82704a 100644 --- a/prefs.py +++ b/prefs.py @@ -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('<>', 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('', self.hotkeylisten) event.widget.bind('', 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('') event.widget.unbind('') @@ -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. diff --git a/protocol.py b/protocol.py index cec4333a..774a012e 100644 --- a/protocol.py +++ b/protocol.py @@ -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. diff --git a/requirements-dev.txt b/requirements-dev.txt index 7ad17560..fba501f3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 19a5efb6..02f928e3 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -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. diff --git a/stats.py b/stats.py index c16ec880..67fc9379 100644 --- a/stats.py +++ b/stats.py @@ -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 diff --git a/td.py b/td.py index 484e8d29..a5f0d6cd 100644 --- a/td.py +++ b/td.py @@ -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# \n\n'.encode('utf-8') + b'#\n# \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() ) diff --git a/tests/journal_lock.py/test_journal_lock.py b/tests/journal_lock.py/test_journal_lock.py index 649140c8..438d1dd5 100644 --- a/tests/journal_lock.py/test_journal_lock.py +++ b/tests/journal_lock.py/test_journal_lock.py @@ -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 diff --git a/theme.py b/theme.py index 20ffe51f..fe431ebb 100644 --- a/theme.py +++ b/theme.py @@ -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 diff --git a/util_ships.py b/util_ships.py index 8abbe20f..4f99e731 100644 --- a/util_ships.py +++ b/util_ships.py @@ -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(