diff --git a/.github/workflows/push-checks.yml b/.github/workflows/push-checks.yml index a15a718c..e238ad8f 100644 --- a/.github/workflows/push-checks.yml +++ b/.github/workflows/push-checks.yml @@ -69,3 +69,7 @@ jobs: - name: mypy type checks run: | ./scripts/mypy-all.sh --platform win32 + + - name: translation checks + run: | + python ./scripts/find_localised_strings.py --compare-lang L10n/en.template --directory . --ignore coriolis-data diff --git a/ChangeLog.md b/ChangeLog.md index 1f4defc8..664f95f8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,6 +6,31 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are in the source (not distributed with the Windows installer) for the currently used version. --- +Release 5.11.1 +=== + +This release fixes a bug regarding FDevID files when running from Source in a non-writable location. Additionally, +Deprecation Warnings are now more visible to aid in plugin development. + +**Changes and Enhancements** +* Added a check on Git Pushes to check for updated translation strings for developers +* Enabled deprecation warnings to pass to plugins and logs +* Updated Dependencies +* Replaced infi.systray with drop-in replacement simplesystray + +**Bug Fixes** +* Fixed a bug that could result in the program not updating or writing FDevID files when running from source in a location where the running user can't write to + +**Plugin Developers** +* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu +* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button +* Calling internal translations with `_()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to importing `translations` and calling `translations.translate` or `translations.tl` directly +* `Translations` as the translate system singleton is deprecated, and is slated for removal in 6.0 or later. Please migrate to the `translations` singleton +* `help_open_log_folder()` is deprecated, and is slated for removal in 6.0 or later. Please migrate to open_folder() +* `update_feed` is deprecated, and is slated for removal in 6.0 or later. Please migrate to `get_update_feed()`. +* FDevID files (`commodity.csv` and `rare_commodity.csv`) have moved their preferred location to the app dir (same location as default Plugins folder). Please migrate to use `config.app_dir_path`. + + Release 5.11.0 === diff --git a/EDMCLogging.py b/EDMCLogging.py index 74b439f9..b6d4b353 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -42,6 +42,7 @@ import logging.handlers import os import pathlib import tempfile +import warnings from contextlib import suppress from fnmatch import fnmatch # So that any warning about accessing a protected member is only in one place. @@ -99,6 +100,8 @@ logging.Logger.trace = lambda self, message, *args, **kwargs: self._log( # type # MAGIC-CONT: See MAGIC tagged comment in Logger.__init__() logging.Formatter.converter = gmtime +warnings.simplefilter('default', DeprecationWarning) + def _trace_if(self: logging.Logger, condition: str, message: str, *args, **kwargs) -> None: if any(fnmatch(condition, p) for p in config_mod.trace_on): diff --git a/EDMarketConnector.py b/EDMarketConnector.py index f512ccf1..cd76ccc0 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -65,6 +65,7 @@ from config import appversion, appversion_nobuild, config, copyright from EDMCLogging import edmclogger, logger, logging from journal_lock import JournalLock, JournalLockResult +from update import check_for_fdev_updates if __name__ == '__main__': # noqa: C901 # Command-line arguments @@ -395,7 +396,7 @@ if TYPE_CHECKING: from logging import TRACE # type: ignore # noqa: F401 # Needed to update mypy if sys.platform == 'win32': - from infi.systray import SysTrayIcon + from simplesystray import SysTrayIcon # isort: on @@ -451,7 +452,7 @@ class AppWindow: self.prefsdialog = None if sys.platform == 'win32': - from infi.systray import SysTrayIcon + from simplesystray import SysTrayIcon def open_window(systray: 'SysTrayIcon') -> None: self.w.deiconify() @@ -2323,6 +2324,7 @@ sys.path: {sys.path}''' root.after(2, show_killswitch_poppup, root) # Start the main event loop try: + check_for_fdev_updates() root.mainloop() except KeyboardInterrupt: logger.info("Ctrl+C Detected, Attempting Clean Shutdown") diff --git a/build.py b/build.py index 2483a4e0..c5236c5a 100644 --- a/build.py +++ b/build.py @@ -208,5 +208,5 @@ def build() -> None: if __name__ == "__main__": - check_for_fdev_updates() + check_for_fdev_updates(local=True) build() diff --git a/collate.py b/collate.py index caf5994f..380cb8ba 100755 --- a/collate.py +++ b/collate.py @@ -22,6 +22,7 @@ from traceback import print_exc import companion import outfitting +from config import config from edmc_data import companion_category_map, ship_name_map @@ -50,7 +51,10 @@ def addcommodities(data) -> None: # noqa: CCR001 if not data['lastStarport'].get('commodities'): return - commodityfile = pathlib.Path('FDevIDs/commodity.csv') + try: + commodityfile = pathlib.Path(config.app_dir_path / 'FDevIDs' / 'commodity.csv') + except FileNotFoundError: + commodityfile = pathlib.Path('FDevIDs/commodity.csv') commodities = {} # slurp existing diff --git a/companion.py b/companion.py index 934fc8bb..775da295 100644 --- a/companion.py +++ b/companion.py @@ -1203,10 +1203,10 @@ def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully if not commodity_map: # Lazily populate for f in ('commodity.csv', 'rare_commodity.csv'): - if not os.path.isfile(config.respath_path / 'FDevIDs/' / f): + if not os.path.isfile(config.app_dir_path / 'FDevIDs/' / f): logger.warning(f'FDevID file {f} not found! Generating output without these commodity name rewrites.') continue - with open(config.respath_path / 'FDevIDs' / f, 'r') as csvfile: + with open(config.app_dir_path / 'FDevIDs' / f, 'r') as csvfile: reader = csv.DictReader(csvfile) for row in reader: diff --git a/config/__init__.py b/config/__init__.py index 18c77c06..8dfcf46b 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -30,7 +30,6 @@ __all__ = [ 'AbstractConfig', 'config', 'get_update_feed', - 'update_feed' ] import abc @@ -41,7 +40,6 @@ import pathlib import re import subprocess import sys -import traceback import warnings from abc import abstractmethod from typing import Any, Callable, Type, TypeVar @@ -54,7 +52,7 @@ appcmdname = 'EDMC' # # Major.Minor.Patch(-prerelease)(+buildmetadata) # NB: Do *not* import this, use the functions appversion() and appversion_nobuild() -_static_appversion = '5.11.0' +_static_appversion = '5.11.1' _cached_version: semantic_version.Version | None = None copyright = '© 2015-2019 Jonathan Harris, 2020-2024 EDCD' @@ -329,8 +327,8 @@ class AbstractConfig(abc.ABC): :raises OSError: On Windows, if a Registry error occurs. :return: The data or the default. """ - warnings.warn(DeprecationWarning('get is Deprecated. use the specific getter for your type')) - logger.debug('Attempt to use Deprecated get() method\n' + ''.join(traceback.format_stack())) + # DEPRECATED: Migrate to specific type getters. Will remove in 6.0 or later. + warnings.warn('get is Deprecated. use the specific getter for your type', DeprecationWarning, stacklevel=2) if (a_list := self._suppress_call(self.get_list, ValueError, key, default=None)) is not None: return a_list @@ -388,8 +386,8 @@ class AbstractConfig(abc.ABC): See get_int for its replacement. :raises OSError: On Windows, if a Registry error occurs. """ - warnings.warn(DeprecationWarning('getint is Deprecated. Use get_int instead')) - logger.debug('Attempt to use Deprecated getint() method\n' + ''.join(traceback.format_stack())) + # DEPRECATED: Migrate to get_int. Will remove in 6.0 or later. + warnings.warn('getint is Deprecated. Use get_int instead', DeprecationWarning, stacklevel=2) return self.get_int(key, default=default) @@ -446,17 +444,19 @@ class AbstractConfig(abc.ABC): """Close this config and release any associated resources.""" raise NotImplementedError +# DEPRECATED: Password system doesn't do anything. Will remove in 6.0 or later. def get_password(self, account: str) -> None: """Legacy password retrieval.""" - warnings.warn("password subsystem is no longer supported", DeprecationWarning) + warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2) def set_password(self, account: str, password: str) -> None: """Legacy password setting.""" - warnings.warn("password subsystem is no longer supported", DeprecationWarning) + warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2) def delete_password(self, account: str) -> None: """Legacy password deletion.""" - warnings.warn("password subsystem is no longer supported", DeprecationWarning) + warnings.warn("password subsystem is no longer supported", DeprecationWarning, stacklevel=2) +# End Dep Zone def get_config(*args, **kwargs) -> AbstractConfig: @@ -489,5 +489,10 @@ def get_update_feed() -> str: return 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml' -# WARNING: update_feed is deprecated, and will be removed in 6.0 or later. Please migrate to get_update_feed() -update_feed = get_update_feed() +# DEPRECATED: Migrate to get_update_feed(). Will remove in 6.0 or later. +def __getattr__(name: str): + if name == 'update_feed': + warnings.warn('update_feed is deprecated, and will be removed in 6.0 or later. ' + 'Please migrate to get_update_feed()', DeprecationWarning, stacklevel=2) + return get_update_feed() + raise AttributeError(name=name) diff --git a/l10n.py b/l10n.py index 27cd95c5..8613244f 100755 --- a/l10n.py +++ b/l10n.py @@ -86,8 +86,7 @@ class Translations: Use when translation is not desired or not available """ self.translations = {None: {}} - # WARNING: '_' is Deprecated. Will be removed in 6.0 or later. - # Migrate to calling translations.translate or tr.tl directly. + # DEPRECATED: Migrate to translations.translate or tr.tl. Will remove in 6.0 or later. builtins.__dict__['_'] = lambda x: str(x).replace(r'\"', '"').replace('{CR}', '\n') def install(self, lang: str | None = None) -> None: # noqa: CCR001 @@ -131,8 +130,7 @@ class Translations: except Exception: logger.exception(f'Exception occurred while parsing {lang}.strings in plugin {plugin}') - # WARNING: '_' is Deprecated. Will be removed in 6.0 or later. - # Migrate to calling translations.translate or tr.tl directly. + # DEPRECATED: Migrate to translations.translate or tr.tl. Will remove in 6.0 or later. builtins.__dict__['_'] = self.translate def contents(self, lang: str, plugin_path: str | None = None) -> dict[str, str]: @@ -262,16 +260,19 @@ class Translations: class _Locale: """Locale holds a few utility methods to convert data to and from localized versions.""" + # DEPRECATED: Migrate to _Locale.string_from_number. Will remove in 6.0 or later. def stringFromNumber(self, number: float | int, decimals: int | None = None) -> str: # noqa: N802 - warnings.warn(DeprecationWarning('use _Locale.string_from_number instead.')) + warnings.warn('use _Locale.string_from_number instead.', DeprecationWarning, stacklevel=2) return self.string_from_number(number, decimals) # type: ignore + # DEPRECATED: Migrate to _Locale.number_from_string. Will remove in 6.0 or later. def numberFromString(self, string: str) -> int | float | None: # noqa: N802 - warnings.warn(DeprecationWarning('use _Locale.number_from_string instead.')) + warnings.warn('use _Locale.number_from_string instead.', DeprecationWarning, stacklevel=2) return self.number_from_string(string) + # DEPRECATED: Migrate to _Locale.preferred_languages. Will remove in 6.0 or later. def preferredLanguages(self) -> Iterable[str]: # noqa: N802 - warnings.warn(DeprecationWarning('use _Locale.preferred_languages instead.')) + warnings.warn('use _Locale.preferred_languages instead.', DeprecationWarning, stacklevel=2) return self.preferred_languages() def string_from_number(self, number: float | int, decimals: int = 5) -> str: @@ -362,13 +363,13 @@ Locale = _Locale() translations = Translations() -# WARNING: 'Translations' singleton is deprecated. Will be removed in 6.0 or later. -# Migrate to importing 'translations'. +# DEPRECATED: Migrate to `translations`. Will be removed in 6.0 or later. +# 'Translations' singleton is deprecated. # Begin Deprecation Zone class _Translations(Translations): def __init__(self): - logger.warning(DeprecationWarning('Translations and _Translations() are deprecated. ' - 'Please use translations and Translations() instead.')) + warnings.warn('Translations and _Translations() are deprecated. ' + 'Please use translations and Translations() instead.', DeprecationWarning, stacklevel=2) super().__init__() diff --git a/myNotebook.py b/myNotebook.py index 0b083c23..c57a6deb 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -11,6 +11,7 @@ from __future__ import annotations import sys import tkinter as tk +import warnings from tkinter import ttk, messagebox from PIL import ImageGrab from l10n import translations as tr @@ -126,6 +127,7 @@ class Entry(EntryMenu): # DEPRECATED: Migrate to EntryMenu. Will remove in 6.0 or later. def __init__(self, master: ttk.Frame | None = None, **kw): + warnings.warn('Migrate to EntryMenu. Will remove in 6.0 or later.', DeprecationWarning, stacklevel=2) EntryMenu.__init__(self, master, **kw) @@ -144,6 +146,7 @@ class ColoredButton(tk.Button): # DEPRECATED: Migrate to tk.Button. Will remove in 6.0 or later. def __init__(self, master: ttk.Frame | None = None, **kw): + warnings.warn('Migrate to tk.Button. Will remove in 6.0 or later.', DeprecationWarning, stacklevel=2) tk.Button.__init__(self, master, **kw) diff --git a/prefs.py b/prefs.py index 7f217ac8..9f2062f4 100644 --- a/prefs.py +++ b/prefs.py @@ -9,6 +9,7 @@ import subprocess import sys import tempfile import tkinter as tk +import warnings from os import system from os.path import expanduser, expandvars, join, normpath from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812 @@ -37,13 +38,11 @@ logger = get_main_logger() # May be imported by plugins - +# DEPRECATED: Migrate to open_log_folder. Will remove in 6.0 or later. def help_open_log_folder() -> None: """Open the folder logs are stored in.""" - logger.warning( - DeprecationWarning("This function is deprecated, use open_log_folder instead. " - "This function will be removed in 6.0 or later") - ) + warnings.warn('prefs.help_open_log_folder is deprecated, use open_log_folder instead. ' + 'This function will be removed in 6.0 or later', DeprecationWarning, stacklevel=2) open_folder(pathlib.Path(tempfile.gettempdir()) / appname) diff --git a/requirements-dev.txt b/requirements-dev.txt index 19e55b66..7c8e1ef5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ wheel # We can't rely on just picking this up from either the base (not venv), # or venv-init-time version. Specify here so that dependabot will prod us # about new versions. -setuptools==69.2.0 +setuptools==70.0.0 # Static analysis tools flake8==7.0.0 diff --git a/requirements.txt b/requirements.txt index 83fa9c0d..22f0a360 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ requests==2.32.3 pillow==10.3.0 watchdog==4.0.0 -infi.systray==0.1.12; sys_platform == 'win32' +simplesystray==0.1.0; sys_platform == 'win32' semantic-version==2.10.0 diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 5911d64c..8187f262 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -1,4 +1,5 @@ """Search all given paths recursively for localised string calls.""" + from __future__ import annotations import argparse @@ -17,20 +18,20 @@ def get_func_name(thing: ast.AST) -> str: if isinstance(thing, ast.Attribute): return get_func_name(thing.value) - return '' + return "" def get_arg(call: ast.Call) -> str: """Extract the argument string to the translate function.""" if len(call.args) > 1: - print('??? > 1 args', call.args, file=sys.stderr) + print("??? > 1 args", call.args, file=sys.stderr) arg = call.args[0] if isinstance(arg, ast.Constant): return arg.value if isinstance(arg, ast.Name): - return f'VARIABLE! CHECK CODE! {arg.id}' - return f'Unknown! {type(arg)=} {ast.dump(arg)} ||| {ast.unparse(arg)}' + return f"VARIABLE! CHECK CODE! {arg.id}" + return f"Unknown! {type(arg)=} {ast.dump(arg)} ||| {ast.unparse(arg)}" def find_calls_in_stmt(statement: ast.AST) -> list[ast.Call]: @@ -38,8 +39,14 @@ def find_calls_in_stmt(statement: ast.AST) -> list[ast.Call]: out = [] for n in ast.iter_child_nodes(statement): out.extend(find_calls_in_stmt(n)) - if isinstance(statement, ast.Call) and get_func_name(statement.func) in ('tr', 'translations'): - if ast.unparse(statement).find('.tl') != -1 or ast.unparse(statement).find('translate') != -1: + if isinstance(statement, ast.Call) and get_func_name(statement.func) in ( + "tr", + "translations", + ): + if ( + ast.unparse(statement).find(".tl") != -1 + or ast.unparse(statement).find("translate") != -1 + ): out.append(statement) return out @@ -53,11 +60,13 @@ COMMENT_OWN_LINE_RE is for a comment on its own line. The difference is necessary in order to tell if a 'above' LANG comment is for its own line (SAME_LINE), or meant to be for this following line (OWN_LINE). """ -COMMENT_SAME_LINE_RE = re.compile(r'^.*?(#.*)$') -COMMENT_OWN_LINE_RE = re.compile(r'^\s*?(#.*)$') +COMMENT_SAME_LINE_RE = re.compile(r"^.*?(#.*)$") +COMMENT_OWN_LINE_RE = re.compile(r"^\s*?(#.*)$") -def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> str | None: # noqa: CCR001 +def extract_comments( # noqa: CCR001 + call: ast.Call, lines: list[str], file: pathlib.Path +) -> str | None: """ Extract comments from source code based on the given call. @@ -83,23 +92,23 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> st match = COMMENT_OWN_LINE_RE.match(above_line) if match: above_comment = match.group(1).strip() - if not above_comment.startswith('# LANG:'): - bad_comment = f'Unknown comment for {file}:{call.lineno} {above_line}' + if not above_comment.startswith("# LANG:"): + bad_comment = f"Unknown comment for {file}:{call.lineno} {above_line}" above_comment = None else: - above_comment = above_comment.replace('# LANG:', '').strip() + above_comment = above_comment.replace("# LANG:", "").strip() if current_line is not None: match = COMMENT_SAME_LINE_RE.match(current_line) if match: current_comment = match.group(1).strip() - if not current_comment.startswith('# LANG:'): - bad_comment = f'Unknown comment for {file}:{call.lineno} {current_line}' + if not current_comment.startswith("# LANG:"): + bad_comment = f"Unknown comment for {file}:{call.lineno} {current_line}" current_comment = None else: - current_comment = current_comment.replace('# LANG:', '').strip() + current_comment = current_comment.replace("# LANG:", "").strip() if current_comment is not None: out = current_comment @@ -109,13 +118,13 @@ def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> st print(bad_comment, file=sys.stderr) if out is None: - print(f'No comment for {file}:{call.lineno} {current_line}', file=sys.stderr) + print(f"No comment for {file}:{call.lineno} {current_line}", file=sys.stderr) return out def scan_file(path: pathlib.Path) -> list[ast.Call]: """Scan a file for ast.Calls.""" - data = path.read_text(encoding='utf-8') + data = path.read_text(encoding="utf-8") lines = data.splitlines() parsed = ast.parse(data) out: list[ast.Call] = [] @@ -125,13 +134,15 @@ def scan_file(path: pathlib.Path) -> list[ast.Call]: # see if we can extract any comments for call in out: - setattr(call, 'comment', extract_comments(call, lines, path)) + setattr(call, "comment", extract_comments(call, lines, path)) out.sort(key=lambda c: c.lineno) return out -def scan_directory(path: pathlib.Path, skip: list[pathlib.Path] | None = None) -> dict[pathlib.Path, list[ast.Call]]: +def scan_directory( + path: pathlib.Path, skip: list[pathlib.Path] | None = None +) -> dict[pathlib.Path, list[ast.Call]]: """ Scan a directory for expected callsites. @@ -145,7 +156,7 @@ def scan_directory(path: pathlib.Path, skip: list[pathlib.Path] | None = None) - if any(same_path.name == thing.name for same_path in skip): continue - if thing.is_file() and thing.suffix == '.py': + if thing.is_file() and thing.suffix == ".py": out[thing] = scan_file(thing) elif thing.is_dir(): out.update(scan_directory(thing, skip)) @@ -163,10 +174,10 @@ def parse_template(path) -> set[str]: """ lang_re = re.compile(r'\s*"([^"]+)"\s*=\s*"([^"]+)"\s*;\s*$') out = set() - with open(path, encoding='utf-8') as file: + with open(path, encoding="utf-8") as file: for line in file: match = lang_re.match(line.strip()) - if match and match.group(1) != '!Language': + if match and match.group(1) != "!Language": out.add(match.group(1)) return out @@ -183,14 +194,16 @@ 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. :param path: Path to the file this FileLocation is in :param c: Call object to extract line information from """ - return FileLocation(path, c.lineno, c.col_offset, c.end_lineno, c.end_col_offset) + return FileLocation( + path, c.lineno, c.col_offset, c.end_lineno, c.end_col_offset + ) @dataclasses.dataclass @@ -238,95 +251,121 @@ def generate_lang_template(data: dict[pathlib.Path, list[ast.Call]]) -> str: entries: list[LangEntry] = [] for path, calls in data.items(): for c in calls: - entries.append(LangEntry([FileLocation.from_call(path, c)], get_arg(c), [getattr(c, 'comment')])) + entries.append( + LangEntry( + [FileLocation.from_call(path, c)], + get_arg(c), + [getattr(c, "comment")], + ) + ) deduped = dedupe_lang_entries(entries) - out = '''/* Language name */ + out = """/* Language name */ "!Language" = "English"; -''' - print(f'Done Deduping entries {len(entries)=} {len(deduped)=}', file=sys.stderr) +""" + print(f"Done Deduping entries {len(entries)=} {len(deduped)=}", file=sys.stderr) for entry in deduped: assert len(entry.comments) == len(entry.locations) comment_set = set() for comment, loc in zip(entry.comments, entry.locations): if comment: - comment_set.add(f'{loc.path.name}: {comment};') + comment_set.add(f"{loc.path.name}: {comment};") - files = 'In files: ' + entry.files() - comment = ' '.join(comment_set).strip() + files = "In files: " + entry.files() + comment = " ".join(comment_set).strip() - header = f'{comment} {files}'.strip() + header = f"{comment} {files}".strip() string = f'"{entry.string}"' - out += f'/* {header} */\n' - out += f'{string} = {string};\n\n' + out += f"/* {header} */\n" + out += f"{string} = {string};\n\n" return out -if __name__ == '__main__': +def main(): # noqa: CCR001 + """Run the Translation Checker.""" parser = argparse.ArgumentParser() - parser.add_argument('--directory', help='Directory to search from', default='.') - parser.add_argument('--ignore', action='append', help='directories to ignore', default=['venv', '.venv', '.git']) + parser.add_argument("--directory", help="Directory to search from", default=".") + parser.add_argument( + "--ignore", + action="append", + help="Directories to ignore", + default=["venv", ".venv", ".git"], + ) group = parser.add_mutually_exclusive_group() - group.add_argument('--json', action='store_true', help='JSON output') - group.add_argument('--lang', help='en.template "strings" output to specified file, "-" for stdout') - group.add_argument('--compare-lang', help='en.template file to compare against') + group.add_argument("--json", action="store_true", help="JSON output") + group.add_argument( + "--lang", help='en.template "strings" output to specified file, "-" for stdout' + ) + group.add_argument("--compare-lang", help="en.template file to compare against") args = parser.parse_args() directory = pathlib.Path(args.directory) res = scan_directory(directory, [pathlib.Path(p) for p in args.ignore]) - if args.compare_lang is not None and len(args.compare_lang) > 0: + output = [] + + if args.compare_lang: seen = set() template = parse_template(args.compare_lang) - for file, calls in res.items(): for c in calls: arg = get_arg(c) if arg in template: seen.add(arg) else: - print(f'NEW! {file}:{c.lineno}: {arg!r}') + output.append(f"NEW! {file}:{c.lineno}: {arg!r}") for old in set(template) ^ seen: - print(f'No longer used: {old!r}') + output.append(f"No longer used: {old!r}") elif args.json: to_print_data = [ { - 'path': str(path), - 'string': get_arg(c), - 'reconstructed': ast.unparse(c), - 'start_line': c.lineno, - 'start_offset': c.col_offset, - 'end_line': c.end_lineno, - 'end_offset': c.end_col_offset, - 'comment': getattr(c, 'comment', None) - } for (path, calls) in res.items() for c in calls + "path": str(path), + "string": get_arg(c), + "reconstructed": ast.unparse(c), + "start_line": c.lineno, + "start_offset": c.col_offset, + "end_line": c.end_lineno, + "end_offset": c.end_col_offset, + "comment": getattr(c, "comment", None), + } + for path, calls in res.items() + for c in calls ] - - print(json.dumps(to_print_data, indent=2)) + output.append(json.dumps(to_print_data, indent=2)) elif args.lang: - if args.lang == '-': - print(generate_lang_template(res)) - + lang_template = generate_lang_template(res) + if args.lang == "-": + output.append(lang_template) else: - with open(args.lang, mode='w+', newline='\n') as langfile: - langfile.writelines(generate_lang_template(res)) + with open(args.lang, mode="w+", newline="\n", encoding="UTF-8") as langfile: + langfile.writelines(lang_template) else: for path, calls in res.items(): - if len(calls) == 0: + if not calls: continue - - print(path) + output.append(str(path)) for c in calls: - print( - f' {c.lineno:4d}({c.col_offset:3d}):{c.end_lineno:4d}({c.end_col_offset:3d})\t', ast.unparse(c) + output.append( + f" {c.lineno:4d}({c.col_offset:3d}):{c.end_lineno:4d}({c.end_col_offset:3d})\t{ast.unparse(c)}" ) + output.append("") - print() + # Print all collected output at the end + if output: + print("\n".join(output)) + sys.exit(1) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + sys.exit() diff --git a/update.py b/update.py index d6364c2d..adef9a80 100644 --- a/update.py +++ b/update.py @@ -8,6 +8,7 @@ See LICENSE file. from __future__ import annotations import pathlib +import shutil import sys import threading from traceback import print_exc @@ -26,23 +27,40 @@ if TYPE_CHECKING: logger = get_main_logger() -def check_for_fdev_updates(silent: bool = False) -> None: # noqa: CCR001 +def check_for_fdev_updates(silent: bool = False, local: bool = False) -> None: # noqa: CCR001 """Check for and download FDEV ID file updates.""" + if local: + pathway = config.respath_path + else: + pathway = config.app_dir_path + files_urls = [ ('commodity.csv', 'https://raw.githubusercontent.com/EDCD/FDevIDs/master/commodity.csv'), ('rare_commodity.csv', 'https://raw.githubusercontent.com/EDCD/FDevIDs/master/rare_commodity.csv') ] for file, url in files_urls: - fdevid_file = pathlib.Path(config.respath_path / 'FDevIDs' / file) + fdevid_file = pathlib.Path(pathway / 'FDevIDs' / file) fdevid_file.parent.mkdir(parents=True, exist_ok=True) try: with open(fdevid_file, newline='', encoding='utf-8') as f: local_content = f.read() except FileNotFoundError: - local_content = None + logger.info(f'File {file} not found. Writing from bundle...') + try: + for localfile in files_urls: + filepath = pathlib.Path(f"FDevIDs/{localfile[0]}") + try: + shutil.copy(filepath, pathway / 'FDevIDs') + except shutil.SameFileError: + logger.info("Not replacing same file...") + fdevid_file = pathlib.Path(pathway / 'FDevIDs' / file) + with open(fdevid_file, newline='', encoding='utf-8') as f: + local_content = f.read() + except FileNotFoundError: + local_content = None - response = requests.get(url) + response = requests.get(url, timeout=20) if response.status_code != 200: if not silent: logger.error(f'Failed to download {file}! Unable to continue.') @@ -169,10 +187,17 @@ class Updater: self.updater.win_sparkle_check_update_with_ui() check_for_fdev_updates() + # TEMP: Only include until 6.0 + try: + check_for_fdev_updates(local=True) + except Exception as e: + logger.info("Tried to update bundle FDEV files but failed. Don't worry, " + "this likely isn't important and can be ignored unless" + f" you run into other issues. If you're curious: {e}") def check_appcast(self) -> EDMCVersion | None: """ - Manually (no Sparkle or WinSparkle) check the update_feed appcast file. + Manually (no Sparkle or WinSparkle) check the get_update_feed() appcast file. Checks if any listed version is semantically greater than the current running version.