From 1fde7b8ba4a00574e1c452965c16c607948c89c0 Mon Sep 17 00:00:00 2001 From: RalfG Date: Fri, 24 Jul 2026 22:47:20 +0200 Subject: [PATCH 1/3] chore: fix automatically fixable ruff linting errors --- ms2rescore/__init__.py | 6 ++--- ms2rescore/__main__.py | 3 +-- ms2rescore/_ristretto_utils.py | 15 ++++++------ ms2rescore/_utils.py | 9 ++++---- ms2rescore/_version.py | 17 ++++++-------- ms2rescore/config_parser.py | 12 ++++------ ms2rescore/core.py | 6 ++--- ms2rescore/exceptions.py | 8 ------- ms2rescore/feature_generators/base.py | 4 +--- ms2rescore/feature_generators/deeplc.py | 8 +++---- ms2rescore/feature_generators/im2deep.py | 9 ++++---- ms2rescore/feature_generators/ms2.py | 5 ++-- ms2rescore/feature_generators/ms2pip.py | 3 +-- ms2rescore/gui/app.py | 26 ++++++++++----------- ms2rescore/gui/function2ctk.py | 4 ++-- ms2rescore/gui/widgets.py | 11 ++++----- ms2rescore/parse_psms.py | 9 ++++---- ms2rescore/parse_spectra.py | 16 ++++++------- ms2rescore/report/charts.py | 29 ++++++++++++------------ ms2rescore/report/data.py | 25 ++++++++++---------- ms2rescore/report/generate.py | 15 +++++------- ms2rescore/report/utils.py | 7 +++--- ms2rescore/rescoring.py | 21 ++++++++--------- 23 files changed, 117 insertions(+), 151 deletions(-) diff --git a/ms2rescore/__init__.py b/ms2rescore/__init__.py index e386c100..602ad616 100644 --- a/ms2rescore/__init__.py +++ b/ms2rescore/__init__.py @@ -27,8 +27,8 @@ module="pyopenms", ) -from ms2rescore._version import get_version # noqa: E402 -from ms2rescore.config_parser import parse_configurations # noqa: E402 -from ms2rescore.core import rescore # noqa: E402 +from ms2rescore._version import get_version +from ms2rescore.config_parser import parse_configurations +from ms2rescore.core import rescore __version__ = get_version() diff --git a/ms2rescore/__main__.py b/ms2rescore/__main__.py index a404b469..93918c2d 100644 --- a/ms2rescore/__main__.py +++ b/ms2rescore/__main__.py @@ -8,7 +8,6 @@ import sys from datetime import datetime from pathlib import Path -from typing import Union from rich.console import Console from rich.logging import RichHandler @@ -164,7 +163,7 @@ def _argument_parser() -> argparse.ArgumentParser: return parser -def _setup_logging(passed_level: str, log_file: Union[str, Path]): +def _setup_logging(passed_level: str, log_file: str | Path): """Setup logging for writing to log file and Rich Console.""" if passed_level not in LOG_MAPPING: raise MS2RescoreConfigurationError( diff --git a/ms2rescore/_ristretto_utils.py b/ms2rescore/_ristretto_utils.py index 4bcbc1a3..2e598191 100644 --- a/ms2rescore/_ristretto_utils.py +++ b/ms2rescore/_ristretto_utils.py @@ -9,7 +9,6 @@ """ import logging -from typing import Dict, Optional, Set import numpy as np import pandas as pd @@ -25,7 +24,7 @@ def _build_features_dataframe( psm_list: PSMList, - feature_names: Set[str], + feature_names: set[str], lower_score_is_better: bool, ) -> pd.DataFrame: """ @@ -71,9 +70,9 @@ def _trim_and_evaluate( max_rank: int, *, run_col: str, - peptide_col: Optional[str], - protein_col: Optional[str], - decoy_pattern: Optional[str], + peptide_col: str | None, + protein_col: str | None, + decoy_pattern: str | None, ) -> RescoreResult: """ Compete to at most ``max_rank`` PSMs per spectrum, then compute q-values/PEP/rollups. @@ -128,7 +127,7 @@ def _is_original_psm(psm) -> bool: return bool(value) -def evaluate_before(psm_list: PSMList, config: Dict) -> RescoreResult: +def evaluate_before(psm_list: PSMList, config: dict) -> RescoreResult: """ Evaluate the PSMs' current (pre-rescoring) score with ristretto, for report baselines. @@ -159,7 +158,7 @@ def evaluate_before(psm_list: PSMList, config: Dict) -> RescoreResult: ) -def evaluate_before_from_provenance(psm_list: PSMList, config: Dict) -> RescoreResult: +def evaluate_before_from_provenance(psm_list: PSMList, config: dict) -> RescoreResult: """ Rebuild the "before" ``RescoreResult`` for standalone report regeneration. @@ -181,7 +180,7 @@ def evaluate_before_from_provenance(psm_list: PSMList, config: Dict) -> RescoreR return evaluate_before(psm_list, config) -def evaluate_after_from_psm_list(psm_list: PSMList, config: Dict) -> RescoreResult: +def evaluate_after_from_psm_list(psm_list: PSMList, config: dict) -> RescoreResult: """ Rebuild the "after" ``RescoreResult`` for standalone report regeneration. diff --git a/ms2rescore/_utils.py b/ms2rescore/_utils.py index dfdc34ad..2b7005e6 100644 --- a/ms2rescore/_utils.py +++ b/ms2rescore/_utils.py @@ -4,23 +4,22 @@ import os from glob import glob from pathlib import Path -from typing import Optional, Union import numpy as np import pandas as pd from ms2rescore_rs import is_supported_file_type from psm_utils import PSMList -from ms2rescore.exceptions import MS2RescoreConfigurationError from ms2rescore._ristretto_utils import _is_original_psm +from ms2rescore.exceptions import MS2RescoreConfigurationError logger = logging.getLogger(__name__) def infer_spectrum_path( - configured_path: Union[str, Path, None], - run_name: Optional[str] = None, -) -> Union[str, Path]: + configured_path: str | Path | None, + run_name: str | None = None, +) -> str | Path: """ Infer spectrum path from passed path and expected filename (e.g. from PSM file). diff --git a/ms2rescore/_version.py b/ms2rescore/_version.py index 0a4d50b1..ad219eb6 100644 --- a/ms2rescore/_version.py +++ b/ms2rescore/_version.py @@ -8,15 +8,13 @@ import importlib.metadata import json import logging +import tomllib as toml from pathlib import Path -from typing import Dict, Optional, Tuple, Union from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen from packaging.version import Version -import tomllib as toml - from ms2rescore.exceptions import MS2RescoreError LOGGER = logging.getLogger(__name__) @@ -28,17 +26,16 @@ class UpdateCheckError(MS2RescoreError): """An error occurred while checking for software updates.""" - pass -def _version_from_metadata() -> Optional[Version]: +def _version_from_metadata() -> Version | None: try: return Version(importlib.metadata.version("ms2rescore")) except importlib.metadata.PackageNotFoundError: return None -def _version_from_pyproject() -> Optional[Version]: +def _version_from_pyproject() -> Version | None: pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml" if not pyproject.is_file(): return None @@ -56,7 +53,7 @@ def _version_from_pyproject() -> Optional[Version]: return None -def _get_latest_version(timeout_seconds: float) -> Tuple[Version, Optional[str]]: +def _get_latest_version(timeout_seconds: float) -> tuple[Version, str | None]: """Check GitHub latest release and return the version string.""" # Prepare GitHub API request url = f"https://api.github.com/repos/{_GITHUB_REPO}/releases/latest" @@ -95,13 +92,13 @@ def get_version() -> str: def check_for_update( - timeout_seconds: Optional[float] = None, -) -> Dict[str, Optional[Union[str, bool]]]: + timeout_seconds: float | None = None, +) -> dict[str, str | bool | None]: """Check GitHub latest release and report whether an update exists.""" timeout_seconds = timeout_seconds or _GITHUB_TIMEOUT_SECONDS # Initialize result dictionary - result: Dict[str, Optional[Union[str, bool]]] = { + result: dict[str, str | bool | None] = { "update_available": False, "current_version": None, "latest_version": None, diff --git a/ms2rescore/config_parser.py b/ms2rescore/config_parser.py index e2fb4f44..5e590251 100644 --- a/ms2rescore/config_parser.py +++ b/ms2rescore/config_parser.py @@ -4,11 +4,9 @@ import json import multiprocessing as mp import re +import tomllib from argparse import Namespace from pathlib import Path -from typing import Dict, List, Union - -import tomllib from cascade_config import CascadeConfig @@ -36,7 +34,7 @@ def _parse_output_path(configured_path, psm_file_path): return (Path(psm_file_path).parent / psm_file_stem).as_posix() -def _validate_filenames(config: Dict) -> Dict: +def _validate_filenames(config: dict) -> dict: """Validate and infer input/output filenames.""" # psm_file should be provided if not config["ms2rescore"]["psm_file"]: @@ -74,7 +72,7 @@ def _validate_filenames(config: Dict) -> Dict: return config -def _validate_processes(config: Dict) -> Dict: +def _validate_processes(config: dict) -> dict: """Validate requested processes with available cpu count.""" n_available = mp.cpu_count() if (config["ms2rescore"]["processes"] == -1) or ( @@ -84,7 +82,7 @@ def _validate_processes(config: Dict) -> Dict: return config -def _validate_regular_expressions(config: Dict) -> Dict: +def _validate_regular_expressions(config: dict) -> dict: """Validate regular expressions in configuration.""" for field in [ "psm_id_pattern", @@ -113,7 +111,7 @@ def _validate_regular_expressions(config: Dict) -> Dict: return config -def parse_configurations(configurations: List[Union[dict, str, Path, Namespace]]) -> Dict: +def parse_configurations(configurations: list[dict | str | Path | Namespace]) -> dict: """ Parse and validate MS²Rescore configuration files and CLI arguments. diff --git a/ms2rescore/core.py b/ms2rescore/core.py index e65b39d1..2c345d56 100644 --- a/ms2rescore/core.py +++ b/ms2rescore/core.py @@ -1,7 +1,6 @@ import json import logging from multiprocessing import cpu_count -from typing import Dict, Optional import psm_utils.io from psm_utils import PSMList @@ -16,7 +15,7 @@ logger = logging.getLogger(__name__) -def rescore(configuration: Dict, psm_list: Optional[PSMList] = None) -> None: +def rescore(configuration: dict, psm_list: PSMList | None = None) -> None: """ Run full MS²Rescore workflow with passed configuration. @@ -277,5 +276,4 @@ def _write_feature_names(feature_names, output_file_root): with open(output_file_root + ".feature_names.tsv", "w") as f: f.write("feature_generator\tfeature_name\n") for fgen, fgen_features in feature_names.items(): - for feature in fgen_features: - f.write(f"{fgen}\t{feature}\n") + f.writelines(f"{fgen}\t{feature}\n" for feature in fgen_features) diff --git a/ms2rescore/exceptions.py b/ms2rescore/exceptions.py index a2dc2d3b..0ef78f4c 100644 --- a/ms2rescore/exceptions.py +++ b/ms2rescore/exceptions.py @@ -4,46 +4,38 @@ class MS2RescoreError(Exception): """Generic MS2Rescore error.""" - pass class MS2RescoreConfigurationError(MS2RescoreError): """Invalid MS2Rescore configuration.""" - pass class IDFileParsingError(MS2RescoreError): """Identification file parsing error.""" - pass class ModificationParsingError(IDFileParsingError): """Identification file parsing error.""" - pass class MissingValuesError(MS2RescoreError): """Missing values in PSMs and/or spectra.""" - pass class ReportGenerationError(MS2RescoreError): """Error while generating report.""" - pass class RescoringError(MS2RescoreError): """Error while rescoring PSMs.""" - pass class ParseSpectrumError(MS2RescoreError): """Error while parsing spectrum files.""" - pass diff --git a/ms2rescore/feature_generators/base.py b/ms2rescore/feature_generators/base.py index 6ab4fb52..23585862 100644 --- a/ms2rescore/feature_generators/base.py +++ b/ms2rescore/feature_generators/base.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from typing import Set from psm_utils import PSMList @@ -10,7 +9,7 @@ class FeatureGeneratorBase(ABC): """Base class from which all feature generators must inherit.""" # List of required MS data types for feature generation - required_ms_data: Set[MSDataType] = set() + required_ms_data: set[MSDataType] = set() def __init__(self, *args, **kwargs) -> None: super().__init__() @@ -28,4 +27,3 @@ def add_features(self, psm_list: PSMList) -> None: class FeatureGeneratorException(Exception): """Base class for exceptions raised by feature generators.""" - pass diff --git a/ms2rescore/feature_generators/deeplc.py b/ms2rescore/feature_generators/deeplc.py index d2b7b7ba..ce44f07a 100644 --- a/ms2rescore/feature_generators/deeplc.py +++ b/ms2rescore/feature_generators/deeplc.py @@ -17,7 +17,7 @@ import logging import warnings -from typing import List, Optional, Union +from typing import Optional import numpy as np from deeplc.calibration import SplineTransformerCalibration @@ -27,9 +27,9 @@ from deeplc.core import _best_correlating_head, finetune, predict from psm_utils import PSMList +from ms2rescore._utils import get_original_hit_mask from ms2rescore.feature_generators.base import FeatureGeneratorBase from ms2rescore.parse_spectra import MSDataType -from ms2rescore._utils import get_original_hit_mask logger = logging.getLogger(__name__) @@ -59,7 +59,7 @@ class DeepLCFeatureGenerator(FeatureGeneratorBase): def __init__( self, *args, - calibration_set_size: Union[int, float, None] = None, + calibration_set_size: float | None = None, processes: int = 1, finetune: Optional[bool] = None, **kwargs, @@ -127,7 +127,7 @@ def _select_deeplc_kwargs(self, keys: tuple, processes: int) -> dict: return selected @property - def feature_names(self) -> List[str]: + def feature_names(self) -> list[str]: return [ "observed_retention_time", "predicted_retention_time", diff --git a/ms2rescore/feature_generators/im2deep.py b/ms2rescore/feature_generators/im2deep.py index 25064b96..2d77e5b7 100644 --- a/ms2rescore/feature_generators/im2deep.py +++ b/ms2rescore/feature_generators/im2deep.py @@ -9,19 +9,18 @@ """ import logging -from typing import List, Union +from pathlib import Path import numpy as np import pandas as pd -from pathlib import Path from im2deep.calibration import LinearCCSCalibration, get_default_reference from im2deep.core import predict from im2deep.utils import im2ccs from psm_utils import PSMList +from ms2rescore._utils import get_original_hit_mask from ms2rescore.feature_generators.base import FeatureGeneratorBase from ms2rescore.parse_spectra import MSDataType -from ms2rescore._utils import get_original_hit_mask logger = logging.getLogger(__name__) @@ -34,7 +33,7 @@ class IM2DeepFeatureGenerator(FeatureGeneratorBase): def __init__( self, multi: bool = False, - calibration_set_size: Union[int, float] = None, + calibration_set_size: float = None, *args, processes: int = 1, **kwargs, @@ -74,7 +73,7 @@ def __init__( self.predict_kwargs["num_threads"] = processes if processes > 0 else None @property - def feature_names(self) -> List[str]: + def feature_names(self) -> list[str]: return [ "ccs_observed_im2deep", "ccs_predicted_im2deep", diff --git a/ms2rescore/feature_generators/ms2.py b/ms2rescore/feature_generators/ms2.py index 9c67a9ac..826a97f4 100644 --- a/ms2rescore/feature_generators/ms2.py +++ b/ms2rescore/feature_generators/ms2.py @@ -4,10 +4,9 @@ """ import logging -from typing import List -from psm_utils import PSMList from ms2rescore_rs import score_ms2_spectra +from psm_utils import PSMList from ms2rescore.feature_generators.base import FeatureGeneratorBase from ms2rescore.parse_spectra import MSDataType @@ -54,7 +53,7 @@ def __init__( self.fragmentation_model = fragmentation_model.lower() @property - def feature_names(self) -> List[str]: + def feature_names(self) -> list[str]: return [ "ln_explained_intensity", "ln_total_intensity", diff --git a/ms2rescore/feature_generators/ms2pip.py b/ms2rescore/feature_generators/ms2pip.py index 39bd3fc4..9a0c6dcb 100644 --- a/ms2rescore/feature_generators/ms2pip.py +++ b/ms2rescore/feature_generators/ms2pip.py @@ -29,7 +29,6 @@ """ import logging -from typing import Optional from ms2pip import correlate from ms2rescore_rs import ( @@ -54,7 +53,7 @@ def __init__( self, *args, model: str = "HCD", - model_dir: Optional[str] = None, + model_dir: str | None = None, processes: int = 1, **kwargs, ) -> None: diff --git a/ms2rescore/gui/app.py b/ms2rescore/gui/app.py index 9ff7ba65..c16d85b7 100644 --- a/ms2rescore/gui/app.py +++ b/ms2rescore/gui/app.py @@ -8,7 +8,7 @@ import sys import webbrowser from pathlib import Path -from typing import Any, Dict, List, Tuple +from typing import Any import customtkinter as ctk from joblib import parallel_backend @@ -18,13 +18,13 @@ from rich.console import Console from rich.logging import RichHandler -import ms2rescore.gui.widgets as widgets import ms2rescore.package_data.img as pkg_data_img from ms2rescore import __version__ as ms2rescore_version from ms2rescore._version import check_for_update from ms2rescore.config_parser import parse_configurations from ms2rescore.core import rescore from ms2rescore.exceptions import MS2RescoreConfigurationError +from ms2rescore.gui import widgets from ms2rescore.gui.function2ctk import Function2CTk, PopupWindow _IMG_DIR = Path(str(importlib.resources.files(pkg_data_img))) @@ -134,7 +134,7 @@ def __init__(self, *args, **kwargs): class LinkFrame(ctk.CTkFrame): - def __init__(self, master, links: List[Tuple[str, str, str]], *args, **kwargs): + def __init__(self, master, links: list[tuple[str, str, str]], *args, **kwargs): super().__init__(master, *args, **kwargs) self.heading = ctk.CTkLabel( self, text="Useful links", font=ctk.CTkFont(weight="bold"), anchor="w" @@ -160,7 +160,7 @@ def __init__(self, master, links: List[Tuple[str, str, str]], *args, **kwargs): class CitationFrame(ctk.CTkFrame): - def __init__(self, master, citations: List[Tuple[str]], *args, **kwargs): + def __init__(self, master, citations: list[tuple[str]], *args, **kwargs): super().__init__(master, *args, **kwargs) self.heading = ctk.CTkLabel( self, text="Please cite", font=ctk.CTkFont(weight="bold"), anchor="w" @@ -310,7 +310,7 @@ def __init__(self, *args, **kwargs): self.fixed_modifications.grid(row=row_n, column=0, pady=(0, 10), sticky="nsew") row_n += 1 - def get(self) -> Dict: + def get(self) -> dict: """Get the configured values as a dictionary.""" try: # there cannot be spaces in the file path @@ -463,7 +463,7 @@ def __init__(self, *args, **kwargs): ) self.config_file.grid(row=8, column=0, columnspan=2, sticky="nsew") - def get(self) -> Dict: + def get(self) -> dict: """Get the configured values as a dictionary.""" return { "rename_to_usi": self.usi.get(), @@ -498,7 +498,7 @@ def __init__(self, *args, **kwargs): self.im2deep_config = Im2DeepConfiguration(self) self.im2deep_config.grid(row=3, column=0, pady=(0, 20), sticky="nsew") - def get(self) -> Dict: + def get(self) -> dict: """Return the configuration as a dictionary.""" basic_enabled, basic_config = self.basic_config.get() ms2pip_enabled, ms2pip_config, annotation_config = self.ms2pip_config.get() @@ -532,7 +532,7 @@ def __init__(self, *args, **kwargs): self.enabled = widgets.LabeledSwitch(self, label="Enable Basic features", default=True) self.enabled.grid(row=1, column=0, pady=(0, 10), sticky="nsew") - def get(self) -> Dict: + def get(self) -> dict: """Return the configuration as a dictionary.""" enabled = self.enabled.get() config = {} @@ -582,7 +582,7 @@ def __init__(self, *args, **kwargs): ) self.tolerance_mode.grid(row=5, column=0, pady=(0, 10), sticky="nsew") - def get(self) -> Dict: + def get(self) -> dict: """Return the configuration as a dictionary.""" enabled = self.enabled.get() feature_config = {"model": self.model.get()} @@ -626,7 +626,7 @@ def __init__(self, *args, **kwargs): ) self.calibration_set_size.grid(row=4, column=0, pady=(0, 10), sticky="nsew") - def get(self) -> Dict: + def get(self) -> dict: """Return the configuration as a dictionary.""" if self.calibration_set_size.get() == "": calibration_set_size = 0.15 @@ -663,10 +663,10 @@ def __init__(self, *args, **kwargs): self.enabled = widgets.LabeledSwitch(self, label="Enable im2deep", default=False) self.enabled.grid(row=1, column=0, pady=(0, 10), sticky="nsew") - def get(self) -> Tuple[bool, Dict[str, Any]]: + def get(self) -> tuple[bool, dict[str, Any]]: """Return the configuration as a dictionary.""" enabled = self.enabled.get() - config: Dict[str, Any] = {} + config: dict[str, Any] = {} return enabled, config @@ -706,7 +706,7 @@ def __init__(self, *args, **kwargs): self.model.grid(row=row_n, column=0, pady=(0, 10), sticky="nsew") row_n += 1 - def get(self) -> Dict: + def get(self) -> dict: """Return the configuration as a dictionary.""" train_fdr_str = self.train_fdr.get() if train_fdr_str == "": diff --git a/ms2rescore/gui/function2ctk.py b/ms2rescore/gui/function2ctk.py index 5d060a44..3eb9d9bb 100644 --- a/ms2rescore/gui/function2ctk.py +++ b/ms2rescore/gui/function2ctk.py @@ -6,7 +6,7 @@ import sys import tkinter as tk import traceback -from typing import Callable, Union +from collections.abc import Callable import customtkinter as ctk @@ -36,7 +36,7 @@ class Function2CTk(ctk.CTk): def __init__( self, sidebar_frame: ctk.CTkFrame, - config_frame: Union[ctk.CTkTabview, ctk.CTkFrame], + config_frame: ctk.CTkTabview | ctk.CTkFrame, function: callable, *args, **kwargs, diff --git a/ms2rescore/gui/widgets.py b/ms2rescore/gui/widgets.py index 382a6ea8..08b97891 100644 --- a/ms2rescore/gui/widgets.py +++ b/ms2rescore/gui/widgets.py @@ -2,7 +2,6 @@ import random import tkinter as tk -from typing import Union import customtkinter as ctk @@ -147,8 +146,8 @@ class FloatSpinbox(ctk.CTkFrame): def __init__( self, *args, - step_size: Union[int, float] = 1, - initial_value: Union[int, float] = 0.0, + step_size: float = 1, + initial_value: float = 0.0, str_format: str = ".2f", width=110, height=32, @@ -193,7 +192,7 @@ def add_button_callback(self): self.entry.delete(0, "end") self.entry.insert(0, format(value, self.str_format)) except ValueError: - return None + return def subtract_button_callback(self): try: @@ -201,9 +200,9 @@ def subtract_button_callback(self): self.entry.delete(0, "end") self.entry.insert(0, format(value, self.str_format)) except ValueError: - return None + return - def get(self) -> Union[float, None]: + def get(self) -> float | None: try: return float(self.entry.get()) except ValueError: diff --git a/ms2rescore/parse_psms.py b/ms2rescore/parse_psms.py index 3f89303c..2775dde5 100644 --- a/ms2rescore/parse_psms.py +++ b/ms2rescore/parse_psms.py @@ -1,6 +1,5 @@ import logging import re -from typing import Dict, Optional, Union import numpy as np import pandas as pd @@ -13,7 +12,7 @@ logger = logging.getLogger(__name__) -def parse_psms(config: Dict, psm_list: Union[PSMList, None]) -> PSMList: +def parse_psms(config: dict, psm_list: PSMList | None) -> PSMList: """ Parse PSMs and prepare for rescoring. @@ -238,7 +237,7 @@ def _n_identified(score: pd.Series) -> int: return lower_score_is_better -def _find_decoys(psm_list: PSMList, id_decoy_pattern: Optional[str] = None): +def _find_decoys(psm_list: PSMList, id_decoy_pattern: str | None = None): """Find decoys in PSMs, log amount, and raise error if none found.""" logger.debug("Finding decoys...") if id_decoy_pattern: @@ -278,8 +277,8 @@ def _match_psm_ids(old_id, regex_pattern): def _parse_values_from_spectrum_id( psm_list: PSMList, - psm_id_rt_pattern: Optional[str] = None, - psm_id_im_pattern: Optional[str] = None, + psm_id_rt_pattern: str | None = None, + psm_id_im_pattern: str | None = None, ): """Parse retention time and or ion mobility values from the spectrum_id.""" for pattern, label, key in zip( diff --git a/ms2rescore/parse_spectra.py b/ms2rescore/parse_spectra.py index 51f7970c..3d4b95dc 100644 --- a/ms2rescore/parse_spectra.py +++ b/ms2rescore/parse_spectra.py @@ -3,7 +3,6 @@ import logging import re from enum import Enum -from typing import Optional, Set import numpy as np from ms2pip._spectrum_processing import proforma_to_mass_shift @@ -11,8 +10,8 @@ from psm_utils import PSMList from rich.progress import track -from ms2rescore.exceptions import MS2RescoreConfigurationError, MS2RescoreError from ms2rescore._utils import infer_spectrum_path +from ms2rescore.exceptions import MS2RescoreConfigurationError, MS2RescoreError LOGGER = logging.getLogger(__name__) @@ -29,7 +28,7 @@ def __str__(self): return self.value -ALL_MS_DATA_TYPES: Set[MSDataType] = { +ALL_MS_DATA_TYPES: set[MSDataType] = { MSDataType.retention_time, MSDataType.ion_mobility, MSDataType.precursor_mz, @@ -39,10 +38,10 @@ def __str__(self): def add_precursor_values( psm_list: PSMList, - required_data_types: Set[MSDataType], - spectrum_path: Optional[str] = None, - spectrum_id_pattern: Optional[str] = None, -) -> Set[MSDataType]: + required_data_types: set[MSDataType], + spectrum_path: str | None = None, + spectrum_id_pattern: str | None = None, +) -> set[MSDataType]: """ Add precursor m/z, retention time, and ion mobility values to a PSM list. @@ -187,7 +186,7 @@ def _acquire_observed_spectra_dict( def _add_precursor_values( - psm_list: PSMList, spectrum_path: str, spectrum_id_pattern: Optional[str] = None + psm_list: PSMList, spectrum_path: str, spectrum_id_pattern: str | None = None ) -> None: """Get precursor m/z, RT, and IM from spectrum files.""" # Iterate over different runs in PSM list @@ -270,4 +269,3 @@ def annotate_spectra( class SpectrumParsingError(MS2RescoreError): """Error while parsing spectrum file.""" - pass diff --git a/ms2rescore/report/charts.py b/ms2rescore/report/charts.py index 69545549..be4e4297 100644 --- a/ms2rescore/report/charts.py +++ b/ms2rescore/report/charts.py @@ -3,7 +3,6 @@ import importlib.resources import warnings from collections import defaultdict -from typing import Dict, List, Optional, Tuple, Union import numpy as np import pandas as pd @@ -123,7 +122,7 @@ def __call__(self, time): return self.y[tind] -def score_histogram(psms: Union[PSMList, pd.DataFrame]) -> go.Figure: +def score_histogram(psms: PSMList | pd.DataFrame) -> go.Figure: """ Plot histogram of scores for a single PSM dataset. @@ -168,7 +167,7 @@ def score_histogram(psms: Union[PSMList, pd.DataFrame]) -> go.Figure: return _style(fig) -def pp_plot(psms: Union[PSMList, pd.DataFrame]) -> go.Figure: +def pp_plot(psms: PSMList | pd.DataFrame) -> go.Figure: """ Generate PP plot of target and decoy score distributions. @@ -228,8 +227,8 @@ def pp_plot(psms: Union[PSMList, pd.DataFrame]) -> go.Figure: def fdr_plot( - psms: Union[PSMList, pd.DataFrame], - fdr_thresholds: Optional[List[float]] = None, + psms: PSMList | pd.DataFrame, + fdr_thresholds: list[float] | None = None, log: bool = True, ) -> go.Figure: """ @@ -273,7 +272,7 @@ def fdr_plot( def feature_weights( - feature_weights: pd.DataFrame, color_discrete_map: Optional[Dict[str, str]] = None + feature_weights: pd.DataFrame, color_discrete_map: dict[str, str] | None = None ) -> go.Figure: """ Plot bar chart of feature weights. @@ -313,7 +312,7 @@ def feature_weights( def feature_weights_by_generator( - feature_weights: pd.DataFrame, color_discrete_map: Optional[Dict[str, str]] = None + feature_weights: pd.DataFrame, color_discrete_map: dict[str, str] | None = None ) -> go.Figure: """ Plot bar chart of feature weights, summed by feature generator. @@ -357,9 +356,9 @@ def feature_weights_by_generator( def ms2pip_correlation( features: pd.DataFrame, - is_decoy: Union[pd.Series, np.ndarray], - qvalue: Union[pd.Series, np.ndarray], - color: Optional[str] = None, + is_decoy: pd.Series | np.ndarray, + qvalue: pd.Series | np.ndarray, + color: str | None = None, ) -> go.Figure: """ Plot MS²PIP correlation for target PSMs with q-value <= 0.01. @@ -397,7 +396,7 @@ def ms2pip_correlation( def calculate_feature_qvalues( features: pd.DataFrame, is_decoy: ArrayLike, -) -> Tuple[pd.DataFrame, pd.DataFrame]: +) -> tuple[pd.DataFrame, pd.DataFrame]: """ Calculate q-values and ECDF AUC for all rescoring features. @@ -472,7 +471,7 @@ def calculate_feature_qvalues( def feature_ecdf_auc_bar( - feature_ecdf_auc: pd.DataFrame, color_discrete_map: Optional[Dict[str, str]] = None + feature_ecdf_auc: pd.DataFrame, color_discrete_map: dict[str, str] | None = None ) -> go.Figure: """ Plot bar chart of feature q-value ECDF AUCs. @@ -509,7 +508,7 @@ def rt_scatter( xaxis_label: str = "Observed retention time", yaxis_label: str = "Predicted retention time", plot_title: str = "Predicted vs. observed retention times", - marker_color: Optional[str] = None, + marker_color: str | None = None, ) -> go.Figure: """ Plot a scatter plot of the predicted vs. observed retention times. @@ -567,7 +566,7 @@ def rt_distribution_baseline( df: pd.DataFrame, predicted_column: str = "Predicted retention time", observed_column: str = "Observed retention time", - highlight_color: Optional[str] = None, + highlight_color: str | None = None, ) -> go.Figure: """ Plot a distribution plot of the relative mean absolute error of the current @@ -839,7 +838,7 @@ def fdr_plot_comparison( return _style(fig) -def _group_keys(df: pd.DataFrame, group_cols: Union[str, List[str]]) -> list: +def _group_keys(df: pd.DataFrame, group_cols: str | list[str]) -> list: """Build hashable group keys from one column, or a compound key from several.""" if isinstance(group_cols, str): return list(df[group_cols]) diff --git a/ms2rescore/report/data.py b/ms2rescore/report/data.py index 611171c7..827045d9 100644 --- a/ms2rescore/report/data.py +++ b/ms2rescore/report/data.py @@ -5,7 +5,6 @@ from collections import defaultdict from dataclasses import dataclass, field from pathlib import Path -from typing import Dict, List, Optional import pandas as pd import psm_utils @@ -58,23 +57,23 @@ class ReportData: """ psm_df: pd.DataFrame - feature_names: Dict[str, List[str]] + feature_names: dict[str, list[str]] before: RescoreResult after: RescoreResult config: dict = field(default_factory=lambda: {"ms2rescore": {}}) - feature_weights: Optional[pd.DataFrame] = None - id_stats: List[dict] = field(default_factory=list) - log_html: Optional[str] = None + feature_weights: pd.DataFrame | None = None + id_stats: list[dict] = field(default_factory=list) + log_html: str | None = None fdr_threshold: float = 0.01 @classmethod def from_run( cls, psm_list: psm_utils.PSMList, - feature_names: Optional[Dict[str, set]] = None, - config: Optional[dict] = None, - before: Optional[RescoreResult] = None, - after: Optional[RescoreResult] = None, + feature_names: dict[str, set] | None = None, + config: dict | None = None, + before: RescoreResult | None = None, + after: RescoreResult | None = None, fdr_threshold: float = 0.01, ) -> "ReportData": """Build report data from an in-memory MS²Rescore run.""" @@ -94,7 +93,7 @@ def from_run( @classmethod def from_files( - cls, output_path_prefix: str, fdr_threshold: Optional[float] = None + cls, output_path_prefix: str, fdr_threshold: float | None = None ) -> "ReportData": """ Build report data by reading the files written by a previous run. @@ -141,14 +140,14 @@ def from_files( ) -def _normalize_feature_names(feature_names: Optional[Dict[str, set]]) -> Dict[str, List[str]]: +def _normalize_feature_names(feature_names: dict[str, set] | None) -> dict[str, list[str]]: """Convert a generator -> feature-name mapping to plain lists, dropping empties.""" if not feature_names: return {} return {gen: list(features) for gen, features in feature_names.items() if features} -def _infer_feature_names(psm_df: pd.DataFrame) -> Dict[str, List[str]]: +def _infer_feature_names(psm_df: pd.DataFrame) -> dict[str, list[str]]: """Infer the generator -> feature-name mapping from the PSM dataframe columns.""" feature_columns = [col for col in psm_df.columns if col not in _NON_FEATURE_COLUMNS] if not feature_columns: @@ -169,7 +168,7 @@ def _infer_feature_names(psm_df: pd.DataFrame) -> Dict[str, List[str]]: return dict(feature_names) -def _read_feature_names_or_infer(path: Path, psm_df: pd.DataFrame) -> Dict[str, List[str]]: +def _read_feature_names_or_infer(path: Path, psm_df: pd.DataFrame) -> dict[str, list[str]]: """Read feature names from file, falling back to inference from the dataframe.""" feature_names = read_feature_names(path) if feature_names: diff --git a/ms2rescore/report/generate.py b/ms2rescore/report/generate.py index 6b227dab..896cfdf6 100644 --- a/ms2rescore/report/generate.py +++ b/ms2rescore/report/generate.py @@ -3,20 +3,17 @@ import importlib.resources import json import logging +import tomllib from datetime import datetime from pathlib import Path -from typing import Optional import pandas as pd from jinja2 import Environment, FileSystemLoader from plotly.offline import get_plotlyjs_version from ristretto import RescoreResult -import tomllib - import ms2rescore -import ms2rescore.report.charts as charts -import ms2rescore.report.templates as templates +from ms2rescore.report import charts, templates from ms2rescore.report.data import ReportData logger = logging.getLogger(__name__) @@ -41,7 +38,7 @@ def generate_report( output_path_prefix: str, data: ReportData, - output_file: Optional[Path] = None, + output_file: Path | None = None, ): """ Generate the HTML report from an in-memory :py:class:`~ms2rescore.report.data.ReportData`. @@ -178,7 +175,7 @@ def _get_target_decoy_context(psm_df: pd.DataFrame, fdr_threshold: float) -> dic def _get_features_context( psm_df: pd.DataFrame, feature_names: dict, - feature_weights: Optional[pd.DataFrame], + feature_weights: pd.DataFrame | None, is_decoy: pd.Series, fdr_threshold: float, ) -> dict: @@ -322,7 +319,7 @@ def _get_config_context(config: dict) -> dict: } -def _get_log_context(output_path_prefix: str, log_html: Optional[str]) -> dict: +def _get_log_context(output_path_prefix: str, log_html: str | None) -> dict: """Return context for the log tab, reading the log file when not provided in memory.""" if log_html is not None: return {"log": log_html} @@ -339,7 +336,7 @@ def _get_log_context(output_path_prefix: str, log_html: Optional[str]) -> dict: return {"log": "Log file could not be found."} -def _render_and_write(output_path_prefix: str, output_file: Optional[Path] = None, **context): +def _render_and_write(output_path_prefix: str, output_file: Path | None = None, **context): """Render the base template with context and write it to the HTML report file.""" if output_file: report_path = Path(output_file).resolve() diff --git a/ms2rescore/report/utils.py b/ms2rescore/report/utils.py index 1cb54c72..bfcffe1b 100644 --- a/ms2rescore/report/utils.py +++ b/ms2rescore/report/utils.py @@ -4,7 +4,6 @@ from collections import defaultdict from csv import DictReader from pathlib import Path -from typing import List, Optional import pandas as pd import psm_utils @@ -22,7 +21,7 @@ _FDR_THRESHOLD = 0.01 -def read_feature_names(feature_names_path: Optional[Path]) -> dict: +def read_feature_names(feature_names_path: Path | None) -> dict: """Read feature names and mapping with feature generator from file.""" feature_names = defaultdict(list) if not feature_names_path or not feature_names_path.is_file(): @@ -52,7 +51,7 @@ def _n_identified(df: pd.DataFrame, fdr_threshold: float) -> int: def compute_protein_stats( before: RescoreResult, after: RescoreResult, fdr_threshold: float = _FDR_THRESHOLD -) -> Optional[List[dict]]: +) -> list[dict] | None: """ Compare protein-group-level identifications before and after rescoring. @@ -73,7 +72,7 @@ def compute_protein_stats( def compute_id_stats( before: RescoreResult, after: RescoreResult, fdr_threshold: float = _FDR_THRESHOLD -) -> List[dict]: +) -> list[dict]: """Build the PSM/peptide/(optional) protein overview stat cards from before/after results.""" stats = [] diff --git a/ms2rescore/rescoring.py b/ms2rescore/rescoring.py index aa226627..2cdc2ddb 100644 --- a/ms2rescore/rescoring.py +++ b/ms2rescore/rescoring.py @@ -12,7 +12,6 @@ import re from concurrent.futures import BrokenExecutor from dataclasses import replace -from typing import Dict, Optional, Tuple import numpy as np import pandas as pd @@ -27,7 +26,7 @@ logger = logging.getLogger(__name__) -def rescore(psm_list: PSMList, config: Dict, output_file_root: str) -> Tuple[PSMList, RescoreResult]: +def rescore(psm_list: PSMList, config: dict, output_file_root: str) -> tuple[PSMList, RescoreResult]: """ Rescore PSMs with ristretto and write the new scores, q-values, and PEPs back to ``psm_list``. @@ -122,7 +121,7 @@ def _write_group_metadata( psms: pd.DataFrame, rollup: pd.DataFrame, group_col: str, - decoy_pattern: Optional[str] = None, + decoy_pattern: str | None = None, ) -> None: """ Write a rollup's score/qvalue/pep onto each PSM's metadata, keyed by ``group_col``. @@ -150,10 +149,10 @@ def _write_group_metadata( def _fix_constant_pep_result( result: RescoreResult, - peptide_col: Optional[str], - protein_col: Optional[str], - decoy_pattern: Optional[str], -) -> Tuple[RescoreResult, Optional[np.ndarray]]: + peptide_col: str | None, + protein_col: str | None, + decoy_pattern: str | None, +) -> tuple[RescoreResult, np.ndarray | None]: """ Detect and fix constant PEP (all 1.0) on a single ``RescoreResult``. @@ -218,10 +217,10 @@ def _fix_constant_pep_result( def _fix_constant_pep( psm_list: PSMList, result: RescoreResult, - peptide_col: Optional[str] = None, - protein_col: Optional[str] = None, - decoy_pattern: Optional[str] = None, -) -> Tuple[PSMList, RescoreResult]: + peptide_col: str | None = None, + protein_col: str | None = None, + decoy_pattern: str | None = None, +) -> tuple[PSMList, RescoreResult]: """ Workaround for broken PEP calculation if the best-scoring PSM is a decoy. From d7b46a15e62ac204dc1fb9984afa2763eff5902e Mon Sep 17 00:00:00 2001 From: RalfG Date: Fri, 24 Jul 2026 22:49:44 +0200 Subject: [PATCH 2/3] chore: fix ruff linting errors without behavior changes --- ms2rescore/config_parser.py | 2 +- ms2rescore/core.py | 6 +- ms2rescore/feature_generators/base.py | 3 +- ms2rescore/feature_generators/deeplc.py | 6 +- ms2rescore/feature_generators/im2deep.py | 5 +- ms2rescore/feature_generators/ms2.py | 3 +- ms2rescore/feature_generators/ms2pip.py | 3 +- ms2rescore/parse_psms.py | 8 +-- ms2rescore/report/__main__.py | 3 +- ms2rescore/report/charts.py | 76 ++++++++++++------------ 10 files changed, 60 insertions(+), 55 deletions(-) diff --git a/ms2rescore/config_parser.py b/ms2rescore/config_parser.py index 5e590251..216b2fdf 100644 --- a/ms2rescore/config_parser.py +++ b/ms2rescore/config_parser.py @@ -148,7 +148,7 @@ def parse_configurations(configurations: list[dict | str | Path | Namespace]) -> continue if isinstance(config, dict): cascade_conf.add_dict(config) - elif isinstance(config, str) or isinstance(config, Path): + elif isinstance(config, (str, Path)): if Path(config).suffix.lower() == ".json": cascade_conf.add_json(config) elif Path(config).suffix.lower() == ".toml": diff --git a/ms2rescore/core.py b/ms2rescore/core.py index 2c345d56..e09d36ef 100644 --- a/ms2rescore/core.py +++ b/ms2rescore/core.py @@ -54,11 +54,11 @@ def rescore(configuration: dict, psm_list: PSMList | None = None) -> None: ) # Define feature names; get existing feature names from PSM file - feature_names = dict() + feature_names = {} psm_list_feature_names = { feature_name for psm_list_features in psm_list["rescoring_features"] - for feature_name in psm_list_features.keys() + for feature_name in psm_list_features } feature_names["psm_file"] = psm_list_feature_names logger.debug( @@ -80,7 +80,7 @@ def rescore(configuration: dict, psm_list: PSMList | None = None) -> None: # Add missing precursor info from spectrum file if needed required_ms_data = { ms_data - for fgen_name in config["feature_generators"].keys() + for fgen_name in config["feature_generators"] if fgen_name not in skip_fgens for ms_data in FEATURE_GENERATORS[fgen_name].required_ms_data } diff --git a/ms2rescore/feature_generators/base.py b/ms2rescore/feature_generators/base.py index 23585862..85b5cff8 100644 --- a/ms2rescore/feature_generators/base.py +++ b/ms2rescore/feature_generators/base.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import ClassVar from psm_utils import PSMList @@ -9,7 +10,7 @@ class FeatureGeneratorBase(ABC): """Base class from which all feature generators must inherit.""" # List of required MS data types for feature generation - required_ms_data: set[MSDataType] = set() + required_ms_data: ClassVar[set[MSDataType]] = set() def __init__(self, *args, **kwargs) -> None: super().__init__() diff --git a/ms2rescore/feature_generators/deeplc.py b/ms2rescore/feature_generators/deeplc.py index ce44f07a..fbcdfd34 100644 --- a/ms2rescore/feature_generators/deeplc.py +++ b/ms2rescore/feature_generators/deeplc.py @@ -17,7 +17,7 @@ import logging import warnings -from typing import Optional +from typing import ClassVar import numpy as np from deeplc.calibration import SplineTransformerCalibration @@ -40,7 +40,7 @@ class DeepLCFeatureGenerator(FeatureGeneratorBase): """DeepLC retention time-based feature generator.""" - required_ms_data = {MSDataType.retention_time} + required_ms_data: ClassVar[set[MSDataType]] = {MSDataType.retention_time} # Flat kwargs forwarded to DeepLC's `predict()` and `finetune()`, keyed by which call(s) each # applies to. `device` and `batch_size` apply to both. getfullargspec(predict).args does not @@ -61,7 +61,7 @@ def __init__( *args, calibration_set_size: float | None = None, processes: int = 1, - finetune: Optional[bool] = None, + finetune: bool | None = None, **kwargs, ) -> None: """ diff --git a/ms2rescore/feature_generators/im2deep.py b/ms2rescore/feature_generators/im2deep.py index 2d77e5b7..ce3a038c 100644 --- a/ms2rescore/feature_generators/im2deep.py +++ b/ms2rescore/feature_generators/im2deep.py @@ -10,6 +10,7 @@ import logging from pathlib import Path +from typing import ClassVar import numpy as np import pandas as pd @@ -28,12 +29,12 @@ class IM2DeepFeatureGenerator(FeatureGeneratorBase): """IM2Deep collision cross section feature generator.""" - required_ms_data = {MSDataType.ion_mobility} + required_ms_data: ClassVar[set[MSDataType]] = {MSDataType.ion_mobility} def __init__( self, multi: bool = False, - calibration_set_size: float = None, + calibration_set_size: float | None = None, *args, processes: int = 1, **kwargs, diff --git a/ms2rescore/feature_generators/ms2.py b/ms2rescore/feature_generators/ms2.py index 826a97f4..46141065 100644 --- a/ms2rescore/feature_generators/ms2.py +++ b/ms2rescore/feature_generators/ms2.py @@ -4,6 +4,7 @@ """ import logging +from typing import ClassVar from ms2rescore_rs import score_ms2_spectra from psm_utils import PSMList @@ -25,7 +26,7 @@ class MS2FeatureGenerator(FeatureGeneratorBase): """MS2 spectrum-based feature generator.""" - required_ms_data = {MSDataType.ms2_spectra} + required_ms_data: ClassVar[set[MSDataType]] = {MSDataType.ms2_spectra} def __init__( self, diff --git a/ms2rescore/feature_generators/ms2pip.py b/ms2rescore/feature_generators/ms2pip.py index 9a0c6dcb..2bfaa135 100644 --- a/ms2rescore/feature_generators/ms2pip.py +++ b/ms2rescore/feature_generators/ms2pip.py @@ -29,6 +29,7 @@ """ import logging +from typing import ClassVar from ms2pip import correlate from ms2rescore_rs import ( @@ -47,7 +48,7 @@ class MS2PIPFeatureGenerator(FeatureGeneratorBase): """Generate MS²PIP-based features from spectra preloaded on the input PSMs.""" - required_ms_data = {MSDataType.ms2_spectra} + required_ms_data: ClassVar[set[MSDataType]] = {MSDataType.ms2_spectra} def __init__( self, diff --git a/ms2rescore/parse_psms.py b/ms2rescore/parse_psms.py index 2775dde5..6f6f2469 100644 --- a/ms2rescore/parse_psms.py +++ b/ms2rescore/parse_psms.py @@ -97,13 +97,13 @@ def parse_psms(config: dict, psm_list: PSMList | None) -> PSMList: # Rename and add modifications logger.debug("Parsing modifications...") - modifications_found = set( - [ + modifications_found = { + re.search(r"\[([^\[\]]*)\]", x.proforma).group(1) for x in psm_list["peptidoform"] if "[" in x.proforma - ] - ) + + } logger.debug(f"Found modifications: {modifications_found}") non_mapped_modifications = modifications_found - set(config["modification_mapping"].keys()) if non_mapped_modifications: diff --git a/ms2rescore/report/__main__.py b/ms2rescore/report/__main__.py index b72a7904..724ee958 100644 --- a/ms2rescore/report/__main__.py +++ b/ms2rescore/report/__main__.py @@ -1,4 +1,5 @@ import logging +import sys from pathlib import Path import click @@ -57,7 +58,7 @@ def main(psm_file, output, fdr): except Exception as e: logger.exception(e) - exit(1) + sys.exit(1) if __name__ == "__main__": diff --git a/ms2rescore/report/charts.py b/ms2rescore/report/charts.py index be4e4297..a99a233c 100644 --- a/ms2rescore/report/charts.py +++ b/ms2rescore/report/charts.py @@ -39,42 +39,42 @@ # Shared Plotly template giving every chart the same typographic and grid style as the report. _TEMPLATE = go.layout.Template( layout=go.Layout( - font=dict(family="Lato, sans-serif", size=13, color="#2b2b2b"), - title=dict( - font=dict(family="Oswald, sans-serif", size=18, color="#1a1a2e"), - x=0.02, - xanchor="left", - ), + font={"family": "Lato, sans-serif", "size": 13, "color": "#2b2b2b"}, + title={ + "font": {"family": "Oswald, sans-serif", "size": 18, "color": "#1a1a2e"}, + "x": 0.02, + "xanchor": "left", + }, paper_bgcolor="white", plot_bgcolor="white", colorway=_COLORWAY, - margin=dict(l=60, r=30, t=60, b=50), - xaxis=dict( - gridcolor="#ececec", - zeroline=False, - showline=True, - linecolor="#cfcfcf", - ticks="outside", - tickcolor="#cfcfcf", - ticklen=4, - automargin=True, - ), - yaxis=dict( - gridcolor="#ececec", - zeroline=False, - showline=True, - linecolor="#cfcfcf", - ticks="outside", - tickcolor="#cfcfcf", - ticklen=4, - automargin=True, - ), - legend=dict( - bgcolor="rgba(255, 255, 255, 0.7)", - bordercolor="#e0e0e0", - borderwidth=1, - ), - hoverlabel=dict(font=dict(family="Lato, sans-serif", size=12), bordercolor="white"), + margin={"l": 60, "r": 30, "t": 60, "b": 50}, + xaxis={ + "gridcolor": "#ececec", + "zeroline": False, + "showline": True, + "linecolor": "#cfcfcf", + "ticks": "outside", + "tickcolor": "#cfcfcf", + "ticklen": 4, + "automargin": True, + }, + yaxis={ + "gridcolor": "#ececec", + "zeroline": False, + "showline": True, + "linecolor": "#cfcfcf", + "ticks": "outside", + "tickcolor": "#cfcfcf", + "ticklen": 4, + "automargin": True, + }, + legend={ + "bgcolor": "rgba(255, 255, 255, 0.7)", + "bordercolor": "#e0e0e0", + "borderwidth": 1, + }, + hoverlabel={"font": {"family": "Lato, sans-serif", "size": 12}, "bordercolor": "white"}, ) ) @@ -205,7 +205,7 @@ def pp_plot(psms: PSMList | pd.DataFrame) -> go.Figure: x=decoy_ecdf, y=target_ecdf, mode="markers", - marker=dict(color=_COLOR_TARGET), + marker={"color": _COLOR_TARGET}, ) ) fig.add_trace( @@ -423,8 +423,8 @@ def calculate_feature_qvalues( Long-form data frame with ECDF AUC for each feature. """ - feature_qvalues = dict() - feature_ecdf_auc = dict() + feature_qvalues = {} + feature_ecdf_auc = {} for fname in features: # Calculate q-values for reversed and non-reversed scores q_values = [] @@ -548,7 +548,7 @@ def rt_scatter( x=[min(df[observed_column]), max(df[observed_column])], y=[min(df[observed_column]), max(df[observed_column])], mode="lines", - line=dict(color=_COLOR_REFERENCE, width=2, dash="dash"), + line={"color": _COLOR_REFERENCE, "width": 2, "dash": "dash"}, ) # Hide legend @@ -920,7 +920,7 @@ def identification_overlap( orientation="h", width=0.4, name=item, - showlegend=True if i == 0 else False, + showlegend=i == 0, ), row=i + 1, col=1, From e4ae8dce49545722a37fdabcffa93de7b4c27a3b Mon Sep 17 00:00:00 2001 From: RalfG Date: Fri, 24 Jul 2026 23:20:09 +0200 Subject: [PATCH 3/3] fix: resolve remaining ruff lint errors in package and tests Address BLE001, SIM115, B006, TRY004, and LOG015 findings: switch blind excepts to logger.exception where traceback logging is useful, add noqa with rationale for genuinely intentional broad catches, replace mutable default arguments, use a module logger instead of the root logger, and use context managers for file opens where the handle doesn't need to outlive the block. Co-authored-by: Copilot --- ms2rescore/__main__.py | 8 ++++---- ms2rescore/_version.py | 1 + ms2rescore/config_parser.py | 5 +++-- ms2rescore/core.py | 6 +++--- ms2rescore/gui/__main__.py | 5 +++-- ms2rescore/gui/app.py | 4 ++-- ms2rescore/gui/function2ctk.py | 4 ++-- ms2rescore/gui/widgets.py | 10 ++++++---- ms2rescore/report/__main__.py | 4 ++-- ms2rescore/report/data.py | 1 + ms2rescore/report/generate.py | 14 +++++++------- tests/test_gui.py | 8 ++++++-- tests/test_report.py | 3 +-- tests/test_version.py | 2 +- 14 files changed, 42 insertions(+), 33 deletions(-) diff --git a/ms2rescore/__main__.py b/ms2rescore/__main__.py index 93918c2d..c3d42dc4 100644 --- a/ms2rescore/__main__.py +++ b/ms2rescore/__main__.py @@ -22,7 +22,7 @@ try: import matplotlib.pyplot as plt - plt.set_loglevel("warning") + plt.set_loglevel("WARNING") except ImportError: pass @@ -189,7 +189,7 @@ def inner(*args, **kwargs): return_value = fnc(*args, **kwargs) # Add timestamp to profiler output filename - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + timestamp = datetime.now().astimezone().strftime("%Y%m%d_%H%M%S") profile_filename = f"{filepath}.profile_{timestamp}.prof" profiler.dump_stats(profile_filename) LOGGER.info(f"Profile data written to: {profile_filename}") @@ -254,8 +254,8 @@ def main(tims=False): profiled_rescore(configuration=config) else: rescore(configuration=config) - except Exception as e: - LOGGER.exception(e) + except Exception: + LOGGER.exception("Unhandled error during rescoring") sys.exit(1) finally: CONSOLE.save_html(config["ms2rescore"]["output_path"] + ".log.html") diff --git a/ms2rescore/_version.py b/ms2rescore/_version.py index ad219eb6..7de4617a 100644 --- a/ms2rescore/_version.py +++ b/ms2rescore/_version.py @@ -125,5 +125,6 @@ def check_for_update( result["update_available"] = latest_version > current_version except Exception: # If current_version can't be parsed, don't treat as updateable + LOGGER.exception("Update check failed") result["update_available"] = False return result diff --git a/ms2rescore/config_parser.py b/ms2rescore/config_parser.py index 216b2fdf..c1c57bec 100644 --- a/ms2rescore/config_parser.py +++ b/ms2rescore/config_parser.py @@ -152,7 +152,8 @@ def parse_configurations(configurations: list[dict | str | Path | Namespace]) -> if Path(config).suffix.lower() == ".json": cascade_conf.add_json(config) elif Path(config).suffix.lower() == ".toml": - cascade_conf.add_dict(dict(tomllib.load(Path(config).open("rb")))) + with Path(config).open("rb") as f: + cascade_conf.add_dict(dict(tomllib.load(f))) else: raise MS2RescoreConfigurationError( "Unknown file extension for configuration file. Should be `json` or `toml`." @@ -160,7 +161,7 @@ def parse_configurations(configurations: list[dict | str | Path | Namespace]) -> elif isinstance(config, Namespace): cascade_conf.add_namespace(config, subkey="ms2rescore") else: - raise ValueError( + raise TypeError( "Configuration should be a dictionary, argparse Namespace, or path to a " "configuration file." ) diff --git a/ms2rescore/core.py b/ms2rescore/core.py index e09d36ef..f36a3eba 100644 --- a/ms2rescore/core.py +++ b/ms2rescore/core.py @@ -207,7 +207,7 @@ def rescore(configuration: dict, psm_list: PSMList | None = None) -> None: # Rename PSMs to USIs if requested, reusing the lookup built above if config["rename_to_usi"]: - logging.debug(f"Creating USIs for {len(psm_list)} PSMs") + logger.debug(f"Creating USIs for {len(psm_list)} PSMs") psm_list["spectrum_id"] = [usi_by_native_id[(psm.run, psm.spectrum_id)] for psm in psm_list] # Rescore PSMs @@ -267,8 +267,8 @@ def rescore(configuration: dict, psm_list: PSMList | None = None) -> None: fdr_threshold=config["report_fdr"], ) generate.generate_report(output_file_root, report_data) - except exceptions.ReportGenerationError as e: - logger.exception(e) + except exceptions.ReportGenerationError: + logger.exception("Report generation failed") def _write_feature_names(feature_names, output_file_root): diff --git a/ms2rescore/gui/__main__.py b/ms2rescore/gui/__main__.py index 583a8565..3b6bc769 100644 --- a/ms2rescore/gui/__main__.py +++ b/ms2rescore/gui/__main__.py @@ -3,6 +3,7 @@ import multiprocessing import os import sys +from pathlib import Path from ms2rescore.gui.app import app @@ -14,9 +15,9 @@ def main(): # 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") + sys.stdout = Path(os.devnull).open("w") # noqa: SIM115 - must stay open for process lifetime if sys.stderr is None: - sys.stderr = open(os.devnull, "w") + sys.stderr = Path(os.devnull).open("w") # noqa: SIM115 - must stay open for process lifetime app() diff --git a/ms2rescore/gui/app.py b/ms2rescore/gui/app.py index c16d85b7..92069f41 100644 --- a/ms2rescore/gui/app.py +++ b/ms2rescore/gui/app.py @@ -757,8 +757,8 @@ def _check_updates_sync(root): UpdateDialog(root, ms2rescore_version, latest, url) # If not ok / offline / rate-limited, we do nothing (no errors to user) except Exception: - # Fully silent on any unexpected issue - pass + # Fully silent to the user; still log for debugging + logger.exception("Update check failed") def _setup_logging(log_level: str, log_file: str) -> Console: diff --git a/ms2rescore/gui/function2ctk.py b/ms2rescore/gui/function2ctk.py index 3eb9d9bb..5b95deed 100644 --- a/ms2rescore/gui/function2ctk.py +++ b/ms2rescore/gui/function2ctk.py @@ -126,7 +126,7 @@ def start_button_callback(self): try: fn_args, fn_kwargs = self.config_frame.get() fn_args = (_apply_selected_log_level(fn_args[0], self.logging_level_selection.get()),) - except Exception as e: + except Exception as e: # noqa: BLE001 - any config-parsing error must be shown to the user self.progress_control.reset() PopupWindow(self, "Error", f"Error occurred while parsing configuration:\n{e}") else: @@ -310,7 +310,7 @@ def run(self): try: self.fn(*self.fn_args, **self.fn_kwargs) except Exception as e: - logger.exception(e) + logger.exception("Unhandled error in worker process") tb = traceback.format_exc() self._cconn.send((e, tb)) diff --git a/ms2rescore/gui/widgets.py b/ms2rescore/gui/widgets.py index 08b97891..50139416 100644 --- a/ms2rescore/gui/widgets.py +++ b/ms2rescore/gui/widgets.py @@ -95,11 +95,12 @@ class LabeledRadioButtons(_LabeledWidget): def __init__( self, *args, - options=[], + options=None, default_value=None, **kwargs, ): super().__init__(*args, **kwargs) + options = options or [] self.value = ctk.StringVar(value=default_value or options[0]) self._radio_buttons = [] for i, option in enumerate(options): @@ -113,8 +114,9 @@ def get(self): class LabeledOptionMenu(_LabeledWidget): - def __init__(self, *args, vertical=False, values=[], default_value=None, **kwargs): + def __init__(self, *args, vertical=False, values=None, default_value=None, **kwargs): super().__init__(*args, **kwargs) + values = values or [] self.value = ctk.StringVar(value=default_value or values[0]) self._option_menu = ctk.CTkOptionMenu(self, variable=self.value, values=values) self._option_menu.grid( @@ -328,7 +330,7 @@ def __init__( self, *args, columns=2, - header_labels=["A", "B"], + header_labels=None, **kwargs, ): """ @@ -344,7 +346,7 @@ def __init__( """ super().__init__(*args, **kwargs) self.columns = columns - self.header_labels = header_labels + self.header_labels = header_labels or ["A", "B"] self.uniform_hash = str(random.getrandbits(128)) diff --git a/ms2rescore/report/__main__.py b/ms2rescore/report/__main__.py index 724ee958..a96b6236 100644 --- a/ms2rescore/report/__main__.py +++ b/ms2rescore/report/__main__.py @@ -56,8 +56,8 @@ def main(psm_file, output, fdr): logger.info(f"✓ Report generated: {output_path}") - except Exception as e: - logger.exception(e) + except Exception: + logger.exception("Report generation failed") sys.exit(1) diff --git a/ms2rescore/report/data.py b/ms2rescore/report/data.py index 827045d9..21ea81e3 100644 --- a/ms2rescore/report/data.py +++ b/ms2rescore/report/data.py @@ -160,6 +160,7 @@ def _infer_feature_names(psm_df: pd.DataFrame) -> dict[str, list[str]]: for feature in generator_class().feature_names: feature_to_generator[feature] = generator_name except Exception: + logger.exception("Could not instantiate feature generator `%s`", generator_name) continue feature_names = defaultdict(list) diff --git a/ms2rescore/report/generate.py b/ms2rescore/report/generate.py index 896cfdf6..92b74c8d 100644 --- a/ms2rescore/report/generate.py +++ b/ms2rescore/report/generate.py @@ -61,7 +61,7 @@ def generate_report( context = { "plotlyjs_version": get_plotlyjs_version(), "metadata": { - "generated_on": datetime.now().strftime("%d/%m/%Y %H:%M:%S"), + "generated_on": datetime.now().astimezone().strftime("%d/%m/%Y %H:%M:%S"), "ms2rescore_version": ms2rescore.__version__, "psm_filename": _get_psm_filenames(data), }, @@ -230,13 +230,13 @@ def _get_features_context( _add_deeplc_chart( context, high_conf_features, fdr_threshold, color=color_map.get("deeplc") ) - except Exception as e: - logger.warning("Could not generate DeepLC performance plot: %s", e) + except Exception: + logger.exception("Could not generate DeepLC performance plot") if "im2deep" in feature_names: try: _add_im2deep_chart(context, high_conf_features, color=color_map.get("im2deep")) - except Exception as e: - logger.warning("Could not generate IM2Deep performance plot: %s", e) + except Exception: + logger.exception("Could not generate IM2Deep performance plot") return context @@ -261,8 +261,8 @@ def _add_feature_weights_chart(context, feature_weights, feature_names_inv, colo ), } ) - except Exception as e: - logger.warning("Could not generate feature weights plot: %s", e) + except Exception: + logger.exception("Could not generate feature weights plot") def _add_deeplc_chart(context, high_conf_features, fdr_threshold, color=None): diff --git a/tests/test_gui.py b/tests/test_gui.py index 14c489af..09d14f6c 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -7,8 +7,12 @@ pytest.importorskip("tkinter") -from ms2rescore.gui.app import DeepLCConfiguration, MS2PIPConfiguration, _setup_logging # noqa: E402 -from ms2rescore.gui.function2ctk import _apply_selected_log_level # noqa: E402 +from ms2rescore.gui.app import ( + DeepLCConfiguration, + MS2PIPConfiguration, + _setup_logging, +) +from ms2rescore.gui.function2ctk import _apply_selected_log_level def test_setup_logging_writes_txt_and_html_log(tmp_path): diff --git a/tests/test_report.py b/tests/test_report.py index 18f2d1fa..1cba8847 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -8,11 +8,10 @@ import pandas as pd import psm_utils.io import pytest +from click.testing import CliRunner from psm_utils import PSM, PSMList from ristretto import RescoreResult -from click.testing import CliRunner - from ms2rescore.report import charts from ms2rescore.report.__main__ import main as report_main from ms2rescore.report.data import ReportData diff --git a/tests/test_version.py b/tests/test_version.py index aae7fab9..6bbd602e 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -64,7 +64,7 @@ def fake_urlopen(req, timeout=None): ua = None try: ua = req.get_header("User-agent") or req.get_header("User-Agent") - except Exception: + except Exception: # noqa: BLE001 - fallback works regardless of urllib Request internals ua = getattr(req, "headers", {}).get("User-Agent") assert ua is not None and "ms2rescore/" in ua return _FakeResp(raw)