diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf3083f..77e3ebf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,8 @@ jobs: run: uv run ms2rescore --help test-windows-installer: + # Only run on push to main (e.g., after PR merge) + if: ${{ github.ref == 'refs/heads/main' }} runs-on: windows-latest steps: - uses: actions/checkout@v4 diff --git a/ms2rescore.spec b/ms2rescore.spec index 4ab7e37..8ecd4eb 100644 --- a/ms2rescore.spec +++ b/ms2rescore.spec @@ -45,7 +45,13 @@ while requirements: checked.add(requirement) module_version = importlib.metadata.version(re.match(r"^[\w\-]+", requirement)[0]) try: - datas_, binaries_, hidden_imports_ = collect_all(requirement, include_py_files=True) + # Use filter to exclude problematic xgboost.testing module + filter_func = lambda name: not name.startswith("xgboost.testing") if requirement == "xgboost" else True + datas_, binaries_, hidden_imports_ = collect_all( + requirement, + include_py_files=True, + filter_submodules=filter_func + ) except (ImportError, RuntimeError) as e: # Skip packages that fail to collect (e.g., xgboost.testing requires hypothesis) print(f"Warning: Failed to collect {requirement}: {e}") @@ -61,6 +67,18 @@ while requirements: hidden_imports = sorted([h for h in hidden_imports if "tests" not in h.split(".")]) hidden_imports = [h for h in hidden_imports if "__pycache__" not in h] + +# Add hdf5plugin imports to fix runtime import issues +hidden_imports.extend([ + "hdf5plugin.plugins.bshuf", + "hdf5plugin.plugins.blosc", + "hdf5plugin.plugins.blosc2", + "hdf5plugin.plugins.lz4", + "hdf5plugin.plugins.fcidecomp", + "hdf5plugin.plugins.zfp", + "hdf5plugin.plugins.zstd", +]) + datas = [ d for d in datas diff --git a/ms2rescore/gui/__main__.py b/ms2rescore/gui/__main__.py index 429e117..583a856 100644 --- a/ms2rescore/gui/__main__.py +++ b/ms2rescore/gui/__main__.py @@ -2,7 +2,7 @@ import multiprocessing import os -import contextlib +import sys from ms2rescore.gui.app import app @@ -10,9 +10,15 @@ def main(): """Entrypoint for MS²Rescore GUI.""" multiprocessing.freeze_support() - # Redirect stdout when running GUI (packaged app might not have console attached) - with contextlib.redirect_stdout(open(os.devnull, "w")): - app() + + # Fix for PyInstaller windowed mode: sys.stdout/stderr can be None + # This causes issues with libraries that try to write to stdout (e.g., Keras progress bars) + if sys.stdout is None: + sys.stdout = open(os.devnull, "w") + if sys.stderr is None: + sys.stderr = open(os.devnull, "w") + + app() if __name__ == "__main__": diff --git a/ms2rescore/gui/app.py b/ms2rescore/gui/app.py index c62e12b..bc781c8 100644 --- a/ms2rescore/gui/app.py +++ b/ms2rescore/gui/app.py @@ -859,6 +859,23 @@ def _check_updates_sync(root): pass +def _setup_logging(log_level: str, log_file: str): + """Setup file logging for GUI.""" + log_level_map = { + "critical": logging.CRITICAL, + "error": logging.ERROR, + "warning": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, + } + file_handler = logging.FileHandler(log_file, mode="w", encoding="utf-8") + file_handler.setFormatter( + logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + ) + file_handler.setLevel(log_level_map.get(log_level, logging.INFO)) + logging.getLogger().addHandler(file_handler) + + def function(config): """Function to be executed in a separate process.""" config = config.copy() @@ -867,6 +884,12 @@ def function(config): else: config_list = [config] config = parse_configurations(config_list) + + # Set up file logging for GUI + _setup_logging( + config["ms2rescore"]["log_level"], config["ms2rescore"]["output_path"] + ".log.txt" + ) + rescore(configuration=config) if config["ms2rescore"]["write_report"]: webbrowser.open_new_tab(config["ms2rescore"]["output_path"] + ".report.html") diff --git a/ms2rescore/report/generate.py b/ms2rescore/report/generate.py index 0043bf4..4a5d411 100644 --- a/ms2rescore/report/generate.py +++ b/ms2rescore/report/generate.py @@ -12,6 +12,7 @@ import plotly.express as px import psm_utils.io from jinja2 import Environment, FileSystemLoader +from plotly.offline import get_plotlyjs_version from psm_utils.psm_list import PSMList try: @@ -93,6 +94,7 @@ def generate_report( log_context = _get_log_context(files) context = { + "plotlyjs_version": get_plotlyjs_version(), "metadata": { "generated_on": datetime.now().strftime("%d/%m/%Y %H:%M:%S"), "ms2rescore_version": ms2rescore.__version__, # TODO: Write during run? @@ -418,8 +420,10 @@ def _render_and_write(output_path_prefix: str, **context): """Render template with context and write to HTML file.""" report_path = Path(output_path_prefix + ".report.html").resolve() logger.info("Writing report to %s", report_path.as_posix()) - template_dir = Path(__file__).parent / "templates" - env = Environment(loader=FileSystemLoader(template_dir, encoding="utf-8")) + + # Use importlib.resources for PyInstaller compatibility + template_dir = importlib.resources.files(templates) + env = Environment(loader=FileSystemLoader(str(template_dir), encoding="utf-8")) template = env.get_template("base.html") with open(report_path, "w", encoding="utf-8") as f: f.write(template.render(**context)) diff --git a/ms2rescore/report/templates/base.html b/ms2rescore/report/templates/base.html index d8e0877..f2f9afc 100644 --- a/ms2rescore/report/templates/base.html +++ b/ms2rescore/report/templates/base.html @@ -11,7 +11,7 @@ - + {% include 'style.html' %}