From ac87cb70777f544f17716212d15ddbcdb538dc7e Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Tue, 1 Sep 2026 18:06:27 +0200 Subject: [PATCH 1/9] WIP --- pyproject.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 05fe9d7..f20527e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,3 +74,17 @@ free_dnn = 'freesas.app.dnn:main' [project.gui-scripts] freesas = 'freesas.app.plot_sas:main' + +[tool.cibuildwheel] +# Skip 32-bit builds and PyPy +skip = ["*-win32", "*-manylinux_i686", "pp*", "*musllinux*"] + +[tool.coverage.run] +branch = true + +[tool.coverage.report] + +ignore_errors = true + +[tool.coverage.html] +directory = "coverage_html_report" From 422d632ab90553b2d5d2ac8379f0bf61a9f3d76d Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Tue, 1 Sep 2026 18:09:23 +0200 Subject: [PATCH 2/9] make compatible with pathlib --- src/freesas/app/bift.py | 4 ++-- src/freesas/containers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/freesas/app/bift.py b/src/freesas/app/bift.py index a77e00f..0932400 100644 --- a/src/freesas/app/bift.py +++ b/src/freesas/app/bift.py @@ -27,7 +27,7 @@ __author__ = "Jérôme Kieffer" __license__ = "MIT" __copyright__ = "2017-2026, ESRF" -__date__ = "06/02/2026" +__date__ = "01/09/2026" import sys import logging @@ -137,7 +137,7 @@ def main(): if logging.root.level < logging.WARNING: traceback.print_exc(file=sys.stdout) else: - dest = afile.stem + ".out" + dest = afile.with_suffix(".out") print(stats.save(dest, source=afile)) diff --git a/src/freesas/containers.py b/src/freesas/containers.py index 1ae5272..b3a4584 100644 --- a/src/freesas/containers.py +++ b/src/freesas/containers.py @@ -30,7 +30,7 @@ __authors__ = ["Jérôme Kieffer"] __license__ = "MIT" __copyright__ = "2020-2026 ESRF" -__date__ = "09/03/2026" +__date__ = "01/09/2026" from collections import namedtuple from typing import NamedTuple @@ -143,7 +143,7 @@ def save(self, filename, source=None): self.density_std.astype(numpy.float32), ): out.write("%s\t%s\t%s%s" % (r, p, s, "\n")) - return filename + ": " + "; ".join(res) + return f"{filename}: " + "; ".join(res) # Used in Cormap From b2ae6784e93f69dface0a430e5a47856f9092e2e Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Tue, 1 Sep 2026 18:14:43 +0200 Subject: [PATCH 3/9] fix ruff corrections --- src/freesas/__init__.py | 3 +-- src/freesas/align.py | 10 ++++++---- src/freesas/app/__init__.py | 1 - src/freesas/app/auto_gpa.py | 6 +++--- src/freesas/app/auto_guinier.py | 6 +++--- src/freesas/app/autorg.py | 6 +++--- src/freesas/app/bift.py | 16 +++++++-------- src/freesas/app/cormap.py | 10 +++++----- src/freesas/app/dnn.py | 6 +++--- src/freesas/app/extract_ascii.py | 15 +++++++------- src/freesas/app/plot_sas.py | 13 +++++++------ src/freesas/app/supycomb.py | 5 +++-- src/freesas/autorg.py | 14 ++++++------- src/freesas/average.py | 1 + src/freesas/bift.py | 6 +++--- src/freesas/containers.py | 11 +++++------ src/freesas/cormap.py | 6 ++++-- src/freesas/decorators.py | 3 +-- src/freesas/dnn.py | 10 ++++++---- src/freesas/fitting.py | 27 ++++++++++++++------------ src/freesas/invariants.py | 3 ++- src/freesas/model.py | 4 ++-- src/freesas/nexus_parser.py | 14 ++++++------- src/freesas/plot.py | 4 ++-- src/freesas/resources/__init__.py | 3 +-- src/freesas/sas_argparser.py | 2 +- src/freesas/sasio.py | 8 ++++---- src/freesas/test/__init__.py | 2 +- src/freesas/test/mock_open_38.py | 3 +-- src/freesas/test/test_align.py | 10 ++++++---- src/freesas/test/test_all.py | 26 +++++++++++++------------ src/freesas/test/test_autorg.py | 13 ++++++------- src/freesas/test/test_average.py | 12 ++++++------ src/freesas/test/test_bift.py | 11 ++++++----- src/freesas/test/test_cormap.py | 8 ++++---- src/freesas/test/test_distance.py | 9 +++++---- src/freesas/test/test_dnn.py | 14 ++++++------- src/freesas/test/test_fitting.py | 27 +++++++++++++------------- src/freesas/test/test_model.py | 13 +++++++------ src/freesas/test/test_resources.py | 4 ++-- src/freesas/test/test_sas_argparser.py | 11 +++++------ src/freesas/test/test_sasio.py | 13 +++++++------ src/freesas/test/utilstest.py | 8 +++----- src/freesas/transformations.py | 1 - 44 files changed, 205 insertions(+), 193 deletions(-) diff --git a/src/freesas/__init__.py b/src/freesas/__init__.py index 80b7c1f..6febf48 100644 --- a/src/freesas/__init__.py +++ b/src/freesas/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2023 European Synchrotron Radiation Facility @@ -28,8 +27,8 @@ __license__ = "MIT" __date__ = "06/02/2026" -import os as _os import logging as _logging +import os as _os _logging.getLogger(__name__).addHandler(_logging.NullHandler()) diff --git a/src/freesas/align.py b/src/freesas/align.py index 84aa075..d6752f8 100644 --- a/src/freesas/align.py +++ b/src/freesas/align.py @@ -2,17 +2,19 @@ __license__ = "MIT" __copyright__ = "2015, ESRF" +import itertools +import logging import os import sys -import numpy + import matplotlib # matplotlib.use('Agg') import matplotlib.pyplot as plt -from freesas.model import SASModel -import itertools +import numpy from scipy.optimize import fmin -import logging + +from freesas.model import SASModel logging.basicConfig(level=logging.INFO) logger = logging.getLogger("log_freesas") diff --git a/src/freesas/app/__init__.py b/src/freesas/app/__init__.py index 4776656..392040b 100644 --- a/src/freesas/app/__init__.py +++ b/src/freesas/app/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/freesas/app/auto_gpa.py b/src/freesas/app/auto_gpa.py index 666afc9..befe102 100644 --- a/src/freesas/app/auto_gpa.py +++ b/src/freesas/app/auto_gpa.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -# coding: utf-8 # # Project: freesas # https://github.com/kif/freesas @@ -29,11 +28,12 @@ __copyright__ = "2021, ESRF" __date__ = "19/03/2021" -import sys import logging +import sys + from freesas.autorg import auto_gpa -from freesas.sas_argparser import GuinierParser from freesas.fitting import run_guinier_fit +from freesas.sas_argparser import GuinierParser logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("auto_gpa") diff --git a/src/freesas/app/auto_guinier.py b/src/freesas/app/auto_guinier.py index b435c97..bb25b98 100644 --- a/src/freesas/app/auto_guinier.py +++ b/src/freesas/app/auto_guinier.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -# coding: utf-8 # # Project: freesas # https://github.com/kif/freesas @@ -29,11 +28,12 @@ __copyright__ = "2021, ESRF" __date__ = "19/03/2021" -import sys import logging +import sys + from freesas.autorg import auto_guinier -from freesas.sas_argparser import GuinierParser from freesas.fitting import run_guinier_fit +from freesas.sas_argparser import GuinierParser logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("auto_guinier") diff --git a/src/freesas/app/autorg.py b/src/freesas/app/autorg.py index 6190b74..bc7b914 100644 --- a/src/freesas/app/autorg.py +++ b/src/freesas/app/autorg.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -# coding: utf-8 # # Project: freesas # https://github.com/kif/freesas @@ -29,11 +28,12 @@ __copyright__ = "2021, ESRF" __date__ = "19/03/2021" -import sys import logging +import sys + from freesas.autorg import autoRg -from freesas.sas_argparser import GuinierParser from freesas.fitting import run_guinier_fit +from freesas.sas_argparser import GuinierParser logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("auto_gpa") diff --git a/src/freesas/app/bift.py b/src/freesas/app/bift.py index 0932400..0204bd9 100644 --- a/src/freesas/app/bift.py +++ b/src/freesas/app/bift.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -# coding: utf-8 # # Project: freesas # https://github.com/kif/freesas @@ -29,19 +28,20 @@ __copyright__ = "2017-2026, ESRF" __date__ = "01/09/2026" -import sys import logging import platform +import sys import traceback + from freesas import bift -from freesas.sasio import ( - load_scattering_data, - convert_inverse_angstrom_to_nanometer, -) -from freesas.sas_argparser import SASParser from freesas.fitting import ( - set_logging_level, collect_files, + set_logging_level, +) +from freesas.sas_argparser import SASParser +from freesas.sasio import ( + convert_inverse_angstrom_to_nanometer, + load_scattering_data, ) logging.basicConfig(level=logging.WARNING) diff --git a/src/freesas/app/cormap.py b/src/freesas/app/cormap.py index 82e5a7f..294a8b9 100644 --- a/src/freesas/app/cormap.py +++ b/src/freesas/app/cormap.py @@ -1,20 +1,20 @@ #!/usr/bin/python3 -# coding: utf-8 __author__ = "Jérôme Kieffer" __license__ = "MIT" __copyright__ = "2015, ESRF" __date__ = "06/02/2026" -import os -import logging import glob +import logging +import os import platform -from itertools import combinations from collections import namedtuple +from itertools import combinations + from freesas.cormap import gof -from freesas.sasio import load_scattering_data from freesas.sas_argparser import SASParser +from freesas.sasio import load_scattering_data logging.basicConfig(level=logging.INFO) logger = logging.getLogger("cormap") diff --git a/src/freesas/app/dnn.py b/src/freesas/app/dnn.py index fb58cf7..95db9c6 100644 --- a/src/freesas/app/dnn.py +++ b/src/freesas/app/dnn.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -# coding: utf-8 # # Project: freesas # https://github.com/kif/freesas @@ -29,10 +28,11 @@ __copyright__ = "2024, ESRF" __date__ = "11/09/2024" -import sys import logging -from freesas.sas_argparser import SASParser +import sys + from freesas.fitting import run_dnn +from freesas.sas_argparser import SASParser logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("free_dnn") diff --git a/src/freesas/app/extract_ascii.py b/src/freesas/app/extract_ascii.py index d859e76..21c23a4 100644 --- a/src/freesas/app/extract_ascii.py +++ b/src/freesas/app/extract_ascii.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -# coding: utf-8 # # Project: freesas # https://github.com/kif/freesas @@ -29,19 +28,21 @@ __copyright__ = "2020-2026, ESRF" __date__ = "06/02/2026" +import copy +import glob import io -import os -import sys +import json import logging -import glob +import os import platform import posixpath -from collections import namedtuple, OrderedDict -import json +import sys import zipfile -import copy +from collections import OrderedDict, namedtuple + import pyFAI from pyFAI.io import Nexus + from freesas.sas_argparser import SASParser logging.basicConfig(level=logging.INFO) diff --git a/src/freesas/app/plot_sas.py b/src/freesas/app/plot_sas.py index 9b5bb62..4e69938 100644 --- a/src/freesas/app/plot_sas.py +++ b/src/freesas/app/plot_sas.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -# coding: utf-8 # # Project: freesas # https://github.com/kif/freesas @@ -31,18 +30,20 @@ __copyright__ = "2020, ESRF" __date__ = "14/05/2020" -import platform import logging +import platform from pathlib import Path -from matplotlib.pyplot import switch_backend + from matplotlib.backends.backend_pdf import PdfPages +from matplotlib.pyplot import switch_backend + from freesas import plot +from freesas.autorg import InsufficientDataError, NoGuinierRegionError +from freesas.sas_argparser import SASParser from freesas.sasio import ( - load_scattering_data, convert_inverse_angstrom_to_nanometer, + load_scattering_data, ) -from freesas.autorg import InsufficientDataError, NoGuinierRegionError -from freesas.sas_argparser import SASParser logging.basicConfig(level=logging.INFO) logger = logging.getLogger("plot_sas") diff --git a/src/freesas/app/supycomb.py b/src/freesas/app/supycomb.py index 53e8343..be1b19a 100644 --- a/src/freesas/app/supycomb.py +++ b/src/freesas/app/supycomb.py @@ -5,8 +5,9 @@ __date__ = "09/07/2020" import logging -from os.path import dirname, abspath -from freesas.align import InputModels, AlignModels +from os.path import abspath, dirname + +from freesas.align import AlignModels, InputModels from freesas.sas_argparser import SASParser base = dirname(dirname(abspath(__file__))) diff --git a/src/freesas/autorg.py b/src/freesas/autorg.py index 3255462..0ecd3fe 100644 --- a/src/freesas/autorg.py +++ b/src/freesas/autorg.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Functions for calculating the radius of gyration and forward scattering intensity.""" __authors__ = ["Jérôme Kieffer"] @@ -7,21 +6,22 @@ __date__ = "06/02/2026" import logging + import numpy from scipy.optimize import curve_fit + from ._autorg import ( # noqa - RG_RESULT, - guinier, - NoGuinierRegionError, DTYPE, + FIT_RESULT, + RG_RESULT, + AutoGuinier, InsufficientDataError, + NoGuinierRegionError, autoRg, - AutoGuinier, + guinier, linear_fit, - FIT_RESULT, ) - logger = logging.getLogger(__name__) diff --git a/src/freesas/average.py b/src/freesas/average.py index 32c0e2a..084dc8f 100644 --- a/src/freesas/average.py +++ b/src/freesas/average.py @@ -3,6 +3,7 @@ __copyright__ = "2015, ESRF" import numpy + from freesas.model import SASModel diff --git a/src/freesas/bift.py b/src/freesas/bift.py index 4e932f1..e4bcab1 100644 --- a/src/freesas/bift.py +++ b/src/freesas/bift.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Bayesian Inverse Fourier Transform @@ -18,10 +17,11 @@ import logging from math import log + from scipy.optimize import minimize -from ._bift import BIFT -from .autorg import auto_guinier, NoGuinierRegionError +from ._bift import BIFT +from .autorg import NoGuinierRegionError, auto_guinier logger = logging.getLogger(__name__) diff --git a/src/freesas/containers.py b/src/freesas/containers.py index b3a4584..f5fa788 100644 --- a/src/freesas/containers.py +++ b/src/freesas/containers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Project: freesas # https://github.com/kif/freesas @@ -34,8 +33,10 @@ from collections import namedtuple from typing import NamedTuple + import numpy + # Used in AutoRg class RG_RESULT(NamedTuple): Rg: float|None=None @@ -134,15 +135,13 @@ def save(self, filename, source=None): ] with open(filename, "wt", encoding="utf-8") as out: out.write("# %s %s" % (source or filename, "\n")) - for txt in res: - out.write(f"# {txt} \n") + out.writelines(f"# {txt} \n" for txt in res) out.write("\n# r\tp(r)\tsigma_p(r)\n") - for r, p, s in zip( + out.writelines("%s\t%s\t%s%s" % (r, p, s, "\n") for r, p, s in zip( self.radius.astype(numpy.float32), self.density_avg.astype(numpy.float32), self.density_std.astype(numpy.float32), - ): - out.write("%s\t%s\t%s%s" % (r, p, s, "\n")) + )) return f"{filename}: " + "; ".join(res) diff --git a/src/freesas/cormap.py b/src/freesas/cormap.py index 517fd53..2e8e624 100644 --- a/src/freesas/cormap.py +++ b/src/freesas/cormap.py @@ -2,10 +2,12 @@ __license__ = "MIT" __copyright__ = "2017, ESRF" -import numpy from math import log -from .containers import GOF + +import numpy + from ._cormap import measure_longest +from .containers import GOF class LongestRunOfHeads: diff --git a/src/freesas/decorators.py b/src/freesas/decorators.py index c135392..6717af3 100644 --- a/src/freesas/decorators.py +++ b/src/freesas/decorators.py @@ -1,4 +1,3 @@ -# coding: utf-8 # # Project: Free SAS tools # https://github.com/kif/freesas @@ -34,9 +33,9 @@ __status__ = "development" __docformat__ = "restructuredtext" +import logging import sys import time -import logging timelog = logging.getLogger("freesas.timeit") diff --git a/src/freesas/dnn.py b/src/freesas/dnn.py index a3d61d4..f4a1f56 100644 --- a/src/freesas/dnn.py +++ b/src/freesas/dnn.py @@ -1,10 +1,12 @@ -import numpy as np +import io import json -import h5py +import os import zipfile + +import h5py +import numpy as np + from .resources import resource_filename -import io -import os # Activation functions diff --git a/src/freesas/fitting.py b/src/freesas/fitting.py index 37ab0a1..58a7d94 100644 --- a/src/freesas/fitting.py +++ b/src/freesas/fitting.py @@ -9,24 +9,27 @@ __status__ = "development" __docformat__ = "restructuredtext" -import sys import logging -import platform import os -from pathlib import Path +import platform +import sys +from collections.abc import Callable, Generator from contextlib import contextmanager -from typing import Callable, List, Optional, IO, Generator +from pathlib import Path +from typing import IO + from numpy import ndarray + from .autorg import ( RG_RESULT, InsufficientDataError, NoGuinierRegionError, ) +from .sas_argparser import GuinierParser, SASParser from .sasio import ( - load_scattering_data, convert_inverse_angstrom_to_nanometer, + load_scattering_data, ) -from .sas_argparser import GuinierParser, SASParser def set_logging_level(verbose_flag: int) -> None: @@ -40,7 +43,7 @@ def set_logging_level(verbose_flag: int) -> None: logging.root.setLevel(logging.DEBUG) -def collect_files(file_list: List[str]) -> List[Path]: +def collect_files(file_list: list[str]) -> list[Path]: """ Take file list from argparser and return list of paths :param file_list: file list as returned by the argparser @@ -55,7 +58,7 @@ def collect_files(file_list: List[str]) -> List[Path]: @contextmanager def get_output_destination( - output_path: Optional[Path] = None, + output_path: Path | None = None, ) -> Generator[IO[str], None, None]: """ Return file or stdout object to write output to @@ -83,7 +86,7 @@ def get_linesep(output_destination: IO[str]) -> str: return "\n" -def get_guinier_header(linesep: str, output_format: Optional[str] = None) -> str: +def get_guinier_header(linesep: str, output_format: str | None = None) -> str: """Return appropriate header line for selected output format :param output_format: output format from string parser :param linesep: correct linesep for chosen destination @@ -109,7 +112,7 @@ def get_guinier_header(linesep: str, output_format: Optional[str] = None) -> str return "" -def get_dnn_header(linesep: str, output_format: Optional[str] = None) -> str: +def get_dnn_header(linesep: str, output_format: str | None = None) -> str: """Return appropriate header line for selected output format :param output_format: output format from string parser :param linesep: correct linesep for chosen destination @@ -134,7 +137,7 @@ def rg_result_to_output_line( rg_result: RG_RESULT, afile: Path, linesep: str, - output_format: Optional[str] = None, + output_format: str | None = None, ) -> str: """Return result line formatted according to selected output format :param rg_result: Result of an rg fit @@ -185,7 +188,7 @@ def dnn_result_to_output_line( dnn_result: tuple, afile: Path, linesep: str, - output_format: Optional[str] = None, + output_format: str | None = None, ) -> str: """Return result line formatted according to selected output format :param dnn_result: Result of an dnn inference, 2 tuple diff --git a/src/freesas/invariants.py b/src/freesas/invariants.py index 5cabc89..3372843 100644 --- a/src/freesas/invariants.py +++ b/src/freesas/invariants.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Project: freesas # https://github.com/kif/freesas @@ -36,7 +35,9 @@ __date__ = "06/02/2026" import logging + import numpy + from .containers import RT_RESULT try: diff --git a/src/freesas/model.py b/src/freesas/model.py index 4e1886d..8a6e486 100644 --- a/src/freesas/model.py +++ b/src/freesas/model.py @@ -1,13 +1,13 @@ #!/usr/bin/env python -# coding: utf-8 __author__ = "Guillaume Bonamis" __license__ = "MIT" __copyright__ = "2015, ESRF" import os -from math import sqrt import threading +from math import sqrt + import numpy try: diff --git a/src/freesas/nexus_parser.py b/src/freesas/nexus_parser.py index 7fb2565..6a90405 100644 --- a/src/freesas/nexus_parser.py +++ b/src/freesas/nexus_parser.py @@ -3,15 +3,15 @@ __copyright__ = "2017, ESRF" __date__ = "06/02/2026" -import sys +import logging import os -import zipfile import posixpath -import logging -from typing import Union -from silx.io.nxdata import NXdata +import sys +import zipfile from dataclasses import dataclass + import numpy +from silx.io.nxdata import NXdata logger = logging.getLogger(__name__) @@ -26,10 +26,10 @@ class IntegratedPattern: """Store one pyFAI integrated pattern""" - point: Union[float, int, None] + point: float | int | None radial: numpy.ndarray intensity: numpy.ndarray - intensity_errors: Union[numpy.ndarray, None] = None + intensity_errors: numpy.ndarray | None = None radial_name: str = "" radial_units: str = "" intensity_name: str = "" diff --git a/src/freesas/plot.py b/src/freesas/plot.py index 1795382..bfa1c0c 100644 --- a/src/freesas/plot.py +++ b/src/freesas/plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Functions to generating graphs related to SAS. """ @@ -9,6 +8,7 @@ __date__ = "09/03/2026" import logging + import numpy from matplotlib.pyplot import subplots @@ -443,7 +443,7 @@ def plot_all( labelsize=None, fontsize=None, ): - from . import bift, autorg + from . import autorg, bift try: guinier = autorg.autoRg(data) diff --git a/src/freesas/resources/__init__.py b/src/freesas/resources/__init__.py index d9a4c26..6525533 100644 --- a/src/freesas/resources/__init__.py +++ b/src/freesas/resources/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (C) 2016-2024 European Synchrotron Radiation Facility @@ -59,9 +58,9 @@ __date__ = "07/03/2024" +import logging import os import sys -import logging logger = logging.getLogger(__name__) diff --git a/src/freesas/sas_argparser.py b/src/freesas/sas_argparser.py index 7a3fd1c..7bb6179 100644 --- a/src/freesas/sas_argparser.py +++ b/src/freesas/sas_argparser.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Generalized arg parser for freeSAS apps to ensure unified command line API. """ @@ -10,6 +9,7 @@ import argparse from pathlib import Path + from freesas import dated_version as freesas_version diff --git a/src/freesas/sasio.py b/src/freesas/sasio.py index 8f39e7d..60b9bcf 100644 --- a/src/freesas/sasio.py +++ b/src/freesas/sasio.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # Project: FreeSAS # https://github.com/kif/freesas @@ -22,9 +21,10 @@ __docformat__ = "restructuredtext" import io -from typing import List, Union from os import PathLike -from numpy import loadtxt, array, ndarray +from typing import Union + +from numpy import array, loadtxt, ndarray PathType = Union[PathLike, str, bytes, io.StringIO, io.BytesIO] @@ -59,7 +59,7 @@ def load_scattering_data(filename: PathType) -> ndarray: return data -def parse_ascii_data(input_file_text: List[str], number_of_columns: int) -> ndarray: +def parse_ascii_data(input_file_text: list[str], number_of_columns: int) -> ndarray: """ Parse data from an ascii file into an N column numpy array diff --git a/src/freesas/test/__init__.py b/src/freesas/test/__init__.py index ff7e07c..edf7cba 100644 --- a/src/freesas/test/__init__.py +++ b/src/freesas/test/__init__.py @@ -1,5 +1,4 @@ #!usr/bin/env python -# coding: utf-8 __author__ = "Jérôme Kieffer" __license__ = "MIT" @@ -8,6 +7,7 @@ import sys import unittest + from .test_all import suite diff --git a/src/freesas/test/mock_open_38.py b/src/freesas/test/mock_open_38.py index a61b188..53a88c4 100644 --- a/src/freesas/test/mock_open_38.py +++ b/src/freesas/test/mock_open_38.py @@ -8,8 +8,7 @@ """ import io -from unittest.mock import MagicMock, DEFAULT - +from unittest.mock import DEFAULT, MagicMock file_spec = None # sentinel = _Sentinel() diff --git a/src/freesas/test/test_align.py b/src/freesas/test/test_align.py index 9b6ebd5..5c840de 100644 --- a/src/freesas/test/test_align.py +++ b/src/freesas/test/test_align.py @@ -4,12 +4,14 @@ __license__ = "MIT" __copyright__ = "2015, ESRF" -import numpy +import logging import unittest -from .utilstest import get_datafile + +import numpy + from ..align import AlignModels -from ..transformations import translation_matrix, euler_matrix -import logging +from ..transformations import euler_matrix, translation_matrix +from .utilstest import get_datafile logging.basicConfig(level=logging.INFO) logger = logging.getLogger("AlignModels_test") diff --git a/src/freesas/test/test_all.py b/src/freesas/test/test_all.py index 01c7681..539a83e 100644 --- a/src/freesas/test/test_all.py +++ b/src/freesas/test/test_all.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 __author__ = "Guillaume" __license__ = "MIT" @@ -7,17 +6,20 @@ __date__ = "03/07/2024" import unittest -from . import test_model -from . import test_align -from . import test_distance -from . import test_cormap -from . import test_autorg -from . import test_bift -from . import test_sasio -from . import test_sas_argparser -from . import test_fitting -from . import test_resources -from . import test_dnn + +from . import ( + test_align, + test_autorg, + test_bift, + test_cormap, + test_distance, + test_dnn, + test_fitting, + test_model, + test_resources, + test_sas_argparser, + test_sasio, +) def suite(): diff --git a/src/freesas/test/test_autorg.py b/src/freesas/test/test_autorg.py index d26c08f..a78dd91 100644 --- a/src/freesas/test/test_autorg.py +++ b/src/freesas/test/test_autorg.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Project: freesas # https://github.com/kif/freesas @@ -29,22 +28,22 @@ import logging import unittest -from math import sqrt, pi +from math import pi, sqrt import numpy from scipy.stats import linregress -from .utilstest import get_datafile +from .._autorg import curate_data # pylint: disable=E0401 +from .._bift import distribution_sphere # pylint: disable=E0401 from ..autorg import ( - autoRg, RG_RESULT, - linear_fit, auto_gpa, auto_guinier, + autoRg, + linear_fit, ) -from .._autorg import curate_data # pylint: disable=E0401 from ..invariants import calc_Rambo_Tainer -from .._bift import distribution_sphere # pylint: disable=E0401 +from .utilstest import get_datafile logger = logging.getLogger(__name__) diff --git a/src/freesas/test/test_average.py b/src/freesas/test/test_average.py index 33873c0..e21056a 100644 --- a/src/freesas/test/test_average.py +++ b/src/freesas/test/test_average.py @@ -1,17 +1,17 @@ #!/usr/bin/python -# coding: utf-8 __author__ = "Guillaume" __license__ = "MIT" __copyright__ = "2015, ESRF" -import numpy +import logging import unittest -from .utilstest import get_datafile -from ..model import SASModel -from ..average import Grid, AverModels -import logging +import numpy + +from ..average import AverModels, Grid +from ..model import SASModel +from .utilstest import get_datafile logging.basicConfig(level=logging.INFO) logger = logging.getLogger("AlignModels_test") diff --git a/src/freesas/test/test_bift.py b/src/freesas/test/test_bift.py index e6918fd..2e8cfaf 100644 --- a/src/freesas/test/test_bift.py +++ b/src/freesas/test/test_bift.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Project: freesas # https://github.com/kif/freesas @@ -29,9 +28,10 @@ import logging import time -import numpy import unittest -from ..bift import auto_bift + +import numpy + from .._bift import ( BIFT, distribution_parabola, @@ -39,6 +39,7 @@ ensure_edges_zero, smooth_density, ) +from ..bift import auto_bift try: from numpy import trapezoid # numpy 2 @@ -55,7 +56,7 @@ class TestBIFT(unittest.TestCase): @classmethod def setUpClass(cls): - super(TestBIFT, cls).setUpClass() + super().setUpClass() cls.r = numpy.linspace(0, cls.DMAX, cls.NPT + 1) dr = cls.DMAX / cls.NPT cls.p = -cls.r * (cls.r - cls.DMAX) # Nice parabola @@ -74,7 +75,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - super(TestBIFT, cls).tearDownClass() + super().tearDownClass() cls.r = cls.p = cls.I = cls.q = cls.err = None def test_autobift(self): diff --git a/src/freesas/test/test_cormap.py b/src/freesas/test/test_cormap.py index c026631..b0cf7da 100644 --- a/src/freesas/test/test_cormap.py +++ b/src/freesas/test/test_cormap.py @@ -1,15 +1,15 @@ #!/usr/bin/python -# coding: utf-8 __author__ = "Jérôme Kieffer" __license__ = "MIT" __copyright__ = "2017-2026, ESRF" -import numpy -import unittest import logging -from .. import cormap +import unittest +import numpy + +from .. import cormap logging.basicConfig(level=logging.INFO) logger = logging.getLogger("test_cormap") diff --git a/src/freesas/test/test_distance.py b/src/freesas/test/test_distance.py index cc5a3e2..0bfe566 100644 --- a/src/freesas/test/test_distance.py +++ b/src/freesas/test/test_distance.py @@ -1,16 +1,17 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- __author__ = "Jérôme Kieffer" __license__ = "MIT" __copyright__ = "2015, ESRF" __date__ = "09/03/2026" -import numpy +import logging import unittest -from .utilstest import get_datafile + +import numpy + from ..model import SASModel -import logging +from .utilstest import get_datafile logging.basicConfig(level=logging.INFO) logger = logging.getLogger("cdistance_test") diff --git a/src/freesas/test/test_dnn.py b/src/freesas/test/test_dnn.py index f404619..aeb6b14 100644 --- a/src/freesas/test/test_dnn.py +++ b/src/freesas/test/test_dnn.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Project: freesas # https://github.com/kif/freesas @@ -27,22 +26,23 @@ __license__ = "MIT" __date__ = "06/02/2026" -import unittest import logging +import unittest + import numpy as np -from .utilstest import get_datafile -from ..sasio import load_scattering_data + from ..dnn import ( DNN, DenseLayer, forward_propagation, + linear, preprocess, - tanh, relu, sigmoid, - linear, + tanh, ) - +from ..sasio import load_scattering_data +from .utilstest import get_datafile logger = logging.getLogger(__name__) diff --git a/src/freesas/test/test_fitting.py b/src/freesas/test/test_fitting.py index b9a24d8..d0a28e9 100644 --- a/src/freesas/test/test_fitting.py +++ b/src/freesas/test/test_fitting.py @@ -1,5 +1,4 @@ #!/usr/bin/python -# coding: utf-8 """Test the functionality of fitting module.""" @@ -8,28 +7,30 @@ __date__ = "12/09/2024" -import unittest -from unittest.mock import patch, MagicMock -import logging -import sys +import contextlib import importlib +import logging +import pathlib import platform +import sys +import unittest +from collections.abc import Callable +from errno import ENOENT from io import StringIO -import pathlib -import contextlib from types import SimpleNamespace -from typing import Callable -from errno import ENOENT +from unittest.mock import MagicMock, patch + import numpy + +from ..autorg import RG_RESULT, InsufficientDataError, NoGuinierRegionError from ..fitting import ( - set_logging_level, - get_output_destination, get_guinier_header, - rg_result_to_output_line, get_linesep, + get_output_destination, + rg_result_to_output_line, run_guinier_fit, + set_logging_level, ) -from ..autorg import RG_RESULT, InsufficientDataError, NoGuinierRegionError from ..sas_argparser import GuinierParser if sys.version_info.minor > 6: diff --git a/src/freesas/test/test_model.py b/src/freesas/test/test_model.py index c95b2b0..8951846 100644 --- a/src/freesas/test/test_model.py +++ b/src/freesas/test/test_model.py @@ -1,18 +1,19 @@ #!/usr/bin/env python -# coding: utf-8 __author__ = "Guillaume" __license__ = "MIT" __copyright__ = "2015, ESRF" -import numpy -import unittest +import logging import os import tempfile -from .utilstest import get_datafile +import unittest + +import numpy + from ..model import SASModel -from ..transformations import translation_from_matrix, euler_from_matrix -import logging +from ..transformations import euler_from_matrix, translation_from_matrix +from .utilstest import get_datafile logging.basicConfig(level=logging.INFO) logger = logging.getLogger("SASModel_test") diff --git a/src/freesas/test/test_resources.py b/src/freesas/test/test_resources.py index 13bd3b4..e98a96d 100644 --- a/src/freesas/test/test_resources.py +++ b/src/freesas/test/test_resources.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Project: freesas # https://github.com/kif/freesas @@ -27,9 +26,10 @@ __license__ = "MIT" __date__ = "28/06/2024" -import unittest import logging import os +import unittest + from ..resources import resource_filename logger = logging.getLogger(__name__) diff --git a/src/freesas/test/test_sas_argparser.py b/src/freesas/test/test_sas_argparser.py index 713a074..1714800 100644 --- a/src/freesas/test/test_sas_argparser.py +++ b/src/freesas/test/test_sas_argparser.py @@ -1,5 +1,4 @@ #!/usr/bin/python -# coding: utf-8 """Test the functionality of SASParser and GuinierParser""" @@ -8,14 +7,14 @@ __date__ = "06/02/2026" -import unittest -import logging -import io import contextlib +import io +import logging +import unittest from pathlib import Path -from .. import dated_version as freesas_version -from ..sas_argparser import SASParser, GuinierParser +from .. import dated_version as freesas_version +from ..sas_argparser import GuinierParser, SASParser logger = logging.getLogger(__name__) diff --git a/src/freesas/test/test_sasio.py b/src/freesas/test/test_sasio.py index cc641fc..c635afd 100644 --- a/src/freesas/test/test_sasio.py +++ b/src/freesas/test/test_sasio.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Project: freesas # https://github.com/kif/freesas @@ -27,14 +26,16 @@ __license__ = "MIT" __date__ = "16/09/2022" -import unittest -import logging import io -from numpy import array, allclose +import logging +import unittest + +from numpy import allclose, array + from ..sasio import ( - parse_ascii_data, - load_scattering_data, convert_inverse_angstrom_to_nanometer, + load_scattering_data, + parse_ascii_data, ) logger = logging.getLogger(__name__) diff --git a/src/freesas/test/utilstest.py b/src/freesas/test/utilstest.py index df8efce..275ff37 100644 --- a/src/freesas/test/utilstest.py +++ b/src/freesas/test/utilstest.py @@ -1,13 +1,13 @@ #!usr/bin/env python -# coding: utf-8 __author__ = "Jérôme Kieffer" __license__ = "MIT" __date__ = "06/02/2026" __copyright__ = "2015-2026, ESRF" -import os import logging +import os + from silx.resources import ExternalResources logger = logging.getLogger("utilstest") @@ -59,9 +59,7 @@ def configure(self, parsed_options=None): environment variables """ - if parsed_options is not None and parsed_options.low_mem: - self.TEST_LOW_MEM = True - elif os.environ.get("FREESAS_LOW_MEM", "True") == "False": + if parsed_options is not None and parsed_options.low_mem or os.environ.get("FREESAS_LOW_MEM", "True") == "False": self.TEST_LOW_MEM = True if parsed_options is not None and parsed_options.random: diff --git a/src/freesas/transformations.py b/src/freesas/transformations.py index dfd5b58..8e16d37 100644 --- a/src/freesas/transformations.py +++ b/src/freesas/transformations.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # transformations.py # Copyright (c) 2006-2015, Christoph Gohlke From f439c783250cdec33532d8c67fcda0a708f9a328 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Tue, 1 Sep 2026 18:17:07 +0200 Subject: [PATCH 4/9] fix ruff unsafe fixes --- src/freesas/__init__.py | 2 +- src/freesas/align.py | 39 ++++++++++++++----------------- src/freesas/app/auto_gpa.py | 3 --- src/freesas/app/auto_guinier.py | 3 --- src/freesas/app/autorg.py | 3 --- src/freesas/app/bift.py | 4 ++-- src/freesas/app/cormap.py | 2 +- src/freesas/app/dnn.py | 3 --- src/freesas/app/extract_ascii.py | 31 +++++++++++------------- src/freesas/app/plot_sas.py | 2 +- src/freesas/app/supycomb.py | 6 ++--- src/freesas/autorg.py | 6 ++--- src/freesas/average.py | 10 ++++---- src/freesas/containers.py | 4 ++-- src/freesas/cormap.py | 8 +++---- src/freesas/fitting.py | 21 ++--------------- src/freesas/invariants.py | 2 +- src/freesas/model.py | 2 +- src/freesas/nexus_parser.py | 5 ++-- src/freesas/plot.py | 30 +++++++----------------- src/freesas/resources/__init__.py | 10 +------- src/freesas/sas_argparser.py | 10 +++----- src/freesas/test/mock_open_38.py | 3 +-- src/freesas/test/test_align.py | 9 ++++--- src/freesas/test/test_autorg.py | 6 ++--- src/freesas/test/test_average.py | 3 +-- src/freesas/test/test_bift.py | 2 +- src/freesas/test/test_distance.py | 8 +++---- src/freesas/test/test_dnn.py | 2 +- src/freesas/test/test_model.py | 21 +++++++---------- src/freesas/transformations.py | 6 ++--- 31 files changed, 99 insertions(+), 167 deletions(-) diff --git a/src/freesas/__init__.py b/src/freesas/__init__.py index 6febf48..0f67edf 100644 --- a/src/freesas/__init__.py +++ b/src/freesas/__init__.py @@ -47,5 +47,5 @@ ) except ImportError: raise RuntimeError( - "Do NOT use %s from its sources: build it and use the built version" % project + f"Do NOT use {project} from its sources: build it and use the built version" ) diff --git a/src/freesas/align.py b/src/freesas/align.py index d6752f8..0c85407 100644 --- a/src/freesas/align.py +++ b/src/freesas/align.py @@ -29,7 +29,7 @@ def __init__(self): self.validmodels = [] def __repr_(self): - return "Preparation of %s models for alignment" % len(self.inputfiles) + return f"Preparation of {len(self.inputfiles)} models for alignment" def assign_models(self, molecule=None): """ @@ -52,8 +52,7 @@ def assign_models(self, molecule=None): self.sasmodels.append(model) if len(self.inputfiles) != len(self.sasmodels): logger.error( - "Problem of assignment\n%s models for %s files" - % (len(self.sasmodels), len(self.inputfiles)) + f"Problem of assignment\n{len(self.sasmodels)} models for {len(self.inputfiles)} files" ) elif len(molecule) != 0: @@ -142,14 +141,14 @@ def rfactorplot(self, filename=None, save=False): [0.5, dammif_files + 0.5], [Rmax, Rmax], "-r", - label="R$_{max}$ = %.3f" % Rmax, + label=f"R$_{{max}}$ = {Rmax:.3f}", ) ax2.set_ylabel("R factor in percent") ax2.set_xticks(xticks) ax2.set_xticklabels(labels, rotation=90) ax2.legend(loc=8) - bbox_props = dict(fc="pink", ec="r", lw=1) + bbox_props = {"fc": "pink", "ec": "r", "lw": 1} for i in range(dammif_files): if not self.validmodels[i]: ax2.text( @@ -162,7 +161,7 @@ def rfactorplot(self, filename=None, save=False): size=10, bbox=bbox_props, ) - logger.info("model %s discarded, Rfactor > Rmax" % self.inputfiles[i]) + logger.info(f"model {self.inputfiles[i]} discarded, Rfactor > Rmax") if save: fig.savefig(filename) @@ -193,7 +192,7 @@ def __init__(self, files, slow=True, enantiomorphs=True): self.reference = None def __repr__(self): - return "alignment process for %s models" % len(self.models) + return f"alignment process for {len(self.models)} models" def assign_models(self): """ @@ -211,8 +210,7 @@ def assign_models(self): self.models.append(model) if len(self.inputfiles) != len(self.models): logger.error( - "Problem of assignment\n%s models for %s files" - % (len(self.models), len(self.inputfiles)) + f"Problem of assignment\n{len(self.models)} models for {len(self.inputfiles)} files" ) return self.models @@ -227,7 +225,7 @@ def optimize(self, reference, molecule, symmetry): :return p: transformation parameters optimized :return dist: NSD after optimization """ - p, dist, niter, nfuncalls, warmflag = fmin( + p, dist, niter, _nfuncalls, _warmflag = fmin( reference.dist_after_movement, molecule.can_param, args=(molecule, symmetry), @@ -239,7 +237,7 @@ def optimize(self, reference, molecule, symmetry): if niter == 200: logger.debug("convergence not reached") else: - logger.debug("convergence reach after %s iterations" % niter) + logger.debug(f"convergence reach after {niter} iterations") return p, dist def alignment_sym(self, reference, molecule): @@ -367,7 +365,7 @@ def plotNSDarray(self, rmax=None, filename=None, save=False): ax1.text( i, j, - "%.2f" % nsd, + f"{nsd:.2f}", ha="center", va="center", size=12 * 8 // dammif_files, @@ -375,7 +373,7 @@ def plotNSDarray(self, rmax=None, filename=None, save=False): ax1.text( j, i, - "%.2f" % nsd, + f"{nsd:.2f}", ha="center", va="center", size=12 * 8 // dammif_files, @@ -407,13 +405,13 @@ def plotNSDarray(self, rmax=None, filename=None, save=False): [0.5, dammif_files + 0.5], [nsd_max, nsd_max], "-r", - label="NSD$_{max}$ = %.2f" % nsd_max, + label=f"NSD$_{{max}}$ = {nsd_max:.2f}", ) ax2.set_title("NSD between any model and all others") ax2.set_ylabel("Normalized Spatial Discrepancy") ax2.set_xticks(xticks) ax2.set_xticklabels(labels, rotation=90) - bbox_props = dict(fc="cyan", ec="b", lw=1) + bbox_props = {"fc": "cyan", "ec": "b", "lw": 1} ax2.text( self.reference + 0.95, data[self.reference] / 2, @@ -426,7 +424,7 @@ def plotNSDarray(self, rmax=None, filename=None, save=False): ) ax2.legend(loc=8) - bbox_props = dict(fc="pink", ec="r", lw=1) + bbox_props = {"fc": "pink", "ec": "r", "lw": 1} valid_number = 0 for i in range(dammif_files): if data[i] > nsd_max: @@ -440,14 +438,13 @@ def plotNSDarray(self, rmax=None, filename=None, save=False): size=10, bbox=bbox_props, ) - logger.debug("model %s discarded, nsd > nsd_max" % self.inputfiles[i]) + logger.debug(f"model {self.inputfiles[i]} discarded, nsd > nsd_max") elif not valid_models[i]: if rmax: ax2.text( i + 0.95, data[self.reference] / 2, - "Discarded, Rfactor = %s > Rmax = %s" - % (100.0 * self.models[i].rfactor, rmax), + f"Discarded, Rfactor = {100.0 * self.models[i].rfactor} > Rmax = {rmax}", ha="center", va="center", rotation=90, @@ -469,7 +466,7 @@ def plotNSDarray(self, rmax=None, filename=None, save=False): if valid_models[i] == 1.0: valid_number += 1 - logger.debug("%s valid models" % valid_number) + logger.debug(f"{valid_number} valid models") if save: fig.savefig(filename) @@ -517,7 +514,7 @@ def alignment_reference(self, ref_number=None): molecule = models[i] symmetry, p = self.alignment_sym(reference, molecule) if not self.slow: - p, dist = self.optimize(reference, molecule, symmetry) + p, _dist = self.optimize(reference, molecule, symmetry) molecule.atoms = molecule.transform( p, symmetry ) # molecule sent on its canonical position diff --git a/src/freesas/app/auto_gpa.py b/src/freesas/app/auto_gpa.py index befe102..f24c202 100644 --- a/src/freesas/app/auto_gpa.py +++ b/src/freesas/app/auto_gpa.py @@ -29,7 +29,6 @@ __date__ = "19/03/2021" import logging -import sys from freesas.autorg import auto_gpa from freesas.fitting import run_guinier_fit @@ -38,8 +37,6 @@ logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("auto_gpa") -if sys.version_info < (3, 6): - logger.error("This code uses F-strings and requires Python 3.6+") def build_parser() -> GuinierParser: diff --git a/src/freesas/app/auto_guinier.py b/src/freesas/app/auto_guinier.py index bb25b98..1ba92f6 100644 --- a/src/freesas/app/auto_guinier.py +++ b/src/freesas/app/auto_guinier.py @@ -29,7 +29,6 @@ __date__ = "19/03/2021" import logging -import sys from freesas.autorg import auto_guinier from freesas.fitting import run_guinier_fit @@ -38,8 +37,6 @@ logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("auto_guinier") -if sys.version_info < (3, 6): - logger.error("This code uses F-strings and requires Python 3.6+") def build_parser() -> GuinierParser: diff --git a/src/freesas/app/autorg.py b/src/freesas/app/autorg.py index bc7b914..a05a66e 100644 --- a/src/freesas/app/autorg.py +++ b/src/freesas/app/autorg.py @@ -29,7 +29,6 @@ __date__ = "19/03/2021" import logging -import sys from freesas.autorg import autoRg from freesas.fitting import run_guinier_fit @@ -38,8 +37,6 @@ logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("auto_gpa") -if sys.version_info < (3, 6): - logger.error("This code uses F-strings and requires Python 3.6+") def build_parser() -> GuinierParser: diff --git a/src/freesas/app/bift.py b/src/freesas/app/bift.py index 0204bd9..d8ef40f 100644 --- a/src/freesas/app/bift.py +++ b/src/freesas/app/bift.py @@ -124,7 +124,7 @@ def main(): try: bo = bift.auto_bift(data, npt=args.npt, scan_size=args.scan) except Exception as err: - print("%s: %s %s" % (afile, err.__class__.__name__, err)) + print(f"{afile}: {err.__class__.__name__} {err}") if logging.root.level < logging.WARNING: traceback.print_exc(file=sys.stdout) else: @@ -133,7 +133,7 @@ def main(): args.mc, args.threshold, npt=args.npt ) except RuntimeError as err: - print("%s: %s %s" % (afile, err.__class__.__name__, err)) + print(f"{afile}: {err.__class__.__name__} {err}") if logging.root.level < logging.WARNING: traceback.print_exc(file=sys.stdout) else: diff --git a/src/freesas/app/cormap.py b/src/freesas/app/cormap.py index 294a8b9..77ff229 100644 --- a/src/freesas/app/cormap.py +++ b/src/freesas/app/cormap.py @@ -46,7 +46,7 @@ def parse(): files = glob.glob(args.file[0]) files.sort() input_len = len(files) - logger.debug("%s input files" % input_len) + logger.debug(f"{input_len} input files") return files diff --git a/src/freesas/app/dnn.py b/src/freesas/app/dnn.py index 95db9c6..8157cfd 100644 --- a/src/freesas/app/dnn.py +++ b/src/freesas/app/dnn.py @@ -29,7 +29,6 @@ __date__ = "11/09/2024" import logging -import sys from freesas.fitting import run_dnn from freesas.sas_argparser import SASParser @@ -37,8 +36,6 @@ logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("free_dnn") -if sys.version_info < (3, 6): - logger.error("This code uses F-strings and requires Python 3.6+") def build_parser() -> SASParser: diff --git a/src/freesas/app/extract_ascii.py b/src/freesas/app/extract_ascii.py index 21c23a4..e81e33b 100644 --- a/src/freesas/app/extract_ascii.py +++ b/src/freesas/app/extract_ascii.py @@ -48,8 +48,6 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger("extract_ascii") -if sys.version_info[0] < 3: - logger.error("This code requires Python 3.4+") NexusJuice = namedtuple( "NexusJuice", @@ -268,23 +266,23 @@ def write_ascii(results, output=None, hdr="#", linesep=os.linesep): headers.append(hdr + " " + results["comments"]) else: headers.append(hdr) - headers.append(hdr + " Sample c= %s mg/ml" % results.get("concentration", -1)) + headers.append(hdr + " Sample c= {} mg/ml".format(results.get("concentration", -1))) headers += [hdr, hdr + " Sample environment:"] if "geometry" in results: - headers.append(hdr + " Detector = %s" % results["geometry"]["detector"]) - headers.append(hdr + " SampleDistance = %s" % results["geometry"]["dist"]) - headers.append(hdr + " WaveLength = %s" % results["geometry"]["wavelength"]) + headers.append(hdr + " Detector = {}".format(results["geometry"]["detector"])) + headers.append(hdr + " SampleDistance = {}".format(results["geometry"]["dist"])) + headers.append(hdr + " WaveLength = {}".format(results["geometry"]["wavelength"])) headers.append(hdr) if "comments" in results: - headers.append(hdr + " title = %s" % results["comment"]) + headers.append(hdr + " title = {}".format(results["comment"])) if "to_merge" in results: headers.append( hdr + " Frames merged: " + " ".join([str(i) for i in results["to_merge"]]) ) if "normalization" in results: - headers.append(hdr + " Normalization = %s" % results["normalization"]) + headers.append(hdr + " Normalization = {}".format(results["normalization"])) if "mask" in results: - headers.append(hdr + " Mask = %s" % results["mask"]) + headers.append(hdr + " Mask = {}".format(results["mask"])) headers.append(hdr) headers.append(hdr + (" N 3" if "std" in results else " N 2")) line = hdr + " L " @@ -303,19 +301,18 @@ def write_ascii(results, output=None, hdr="#", linesep=os.linesep): if "storage temperature" in results: headers.append( hdr - + " Storage Temperature (degrees C): %s" % results["storage temperature"] + + " Storage Temperature (degrees C): {}".format(results["storage temperature"]) ) if "exposure temperature" in results: headers.append( hdr - + " Measurement Temperature (degrees C): %s" - % results["exposure temperature"] + + " Measurement Temperature (degrees C): {}".format(results["exposure temperature"]) ) - headers.append(hdr + " Concentration: %s" % results.get("concentration", -1)) + headers.append(hdr + " Concentration: {}".format(results.get("concentration", -1))) if "buffer" in results: - headers.append(hdr + " Buffer: %s" % results["buffer"]) - headers.append(hdr + " Code: %s" % results.get("sample", "")) + headers.append(hdr + " Buffer: {}".format(results["buffer"])) + headers.append(hdr + " Code: {}".format(results.get("sample", ""))) def write(headers, file_): @@ -324,12 +321,12 @@ def write(headers, file_): if "std" in results: data = [ - "%14.6e\t%14.6e\t%14.6e" % (q, intensity, std) + f"{q:14.6e}\t{intensity:14.6e}\t{std:14.6e}" for q, intensity, std in zip(results["q"], results["I"], results["std"]) ] else: data = [ - "%14.6e\t%14.6e\t" % (q, intensity) + f"{q:14.6e}\t{intensity:14.6e}\t" for q, intensity in zip(results["q"], results["I"]) ] data.append("") diff --git a/src/freesas/app/plot_sas.py b/src/freesas/app/plot_sas.py index 4e69938..1c9b99f 100644 --- a/src/freesas/app/plot_sas.py +++ b/src/freesas/app/plot_sas.py @@ -49,7 +49,7 @@ logger = logging.getLogger("plot_sas") -def set_backend(output: Path = None, output_format: str = None): +def set_backend(output: Path | None = None, output_format: str | None = None): """Explicitely set silent backend based on format or filename Needed on MacOS @param output: Name of the specified output file diff --git a/src/freesas/app/supycomb.py b/src/freesas/app/supycomb.py index be1b19a..fec7f25 100644 --- a/src/freesas/app/supycomb.py +++ b/src/freesas/app/supycomb.py @@ -77,7 +77,7 @@ def main(): args = parse() input_len = len(args.file) - logger.info("%s input files" % input_len) + logger.info(f"{input_len} input files") selection = InputModels() if args.mode == "SLOW": @@ -110,8 +110,8 @@ def main(): align.outputfiles = args.output align.assign_models() dist = align.alignment_2models() - logger.info("%s and %s aligned" % (args.file[0], args.file[1])) - logger.info("NSD after optimized alignment = %.2f" % dist) + logger.info(f"{args.file[0]} and {args.file[1]} aligned") + logger.info(f"NSD after optimized alignment = {dist:.2f}") else: align.outputfiles = ["model-%02i.pdb" % (i + 1) for i in range(input_len)] selection.inputfiles = args.file diff --git a/src/freesas/autorg.py b/src/freesas/autorg.py index 0ecd3fe..8891e60 100644 --- a/src/freesas/autorg.py +++ b/src/freesas/autorg.py @@ -152,7 +152,7 @@ def auto_guinier(data, Rg_min=1.0, qRg_max=1.3, relax=1.2): data, q_ary, i_ary, sigma_ary, Rg_min, qRg_max, relax ) if start0 < 0: - raise InsufficientDataError("Minimum region size is %s" % guinier.min_size) + raise InsufficientDataError(f"Minimum region size is {guinier.min_size}") guinier.guinier_space( start0, stop0, q_ary, i_ary, sigma_ary, q2_ary, lnI_ary, wg_ary ) @@ -161,7 +161,7 @@ def auto_guinier(data, Rg_min=1.0, qRg_max=1.3, relax=1.2): q2_ary, lnI_ary, wg_ary, start0, stop0, Rg_min, qRg_max, relax ) - cnt, relaxed, qRg_max, aslope_max = guinier.count_valid(fits, qRg_max, relax) + cnt, _relaxed, qRg_max, _aslope_max = guinier.count_valid(fits, qRg_max, relax) # valid_fits = fits[fits[:, 9] < qRg_max] if cnt == 0: raise NoGuinierRegionError(qRg_max) @@ -170,7 +170,7 @@ def auto_guinier(data, Rg_min=1.0, qRg_max=1.3, relax=1.2): start, stop = guinier.find_region(fits, qRg_max) # Now average out the - Rg_avg, Rg_std, I0_avg, I0_std, good = guinier.average_values(fits, start, stop) + Rg_avg, Rg_std, I0_avg, I0_std, _good = guinier.average_values(fits, start, stop) aggregated = guinier.check_aggregation( q2_ary, lnI_ary, wg_ary, start0, stop, Rg=Rg_avg, threshold=False diff --git a/src/freesas/average.py b/src/freesas/average.py index 084dc8f..186bb8d 100644 --- a/src/freesas/average.py +++ b/src/freesas/average.py @@ -247,10 +247,10 @@ def make_header(self): Create the layout of the pdb file for the averaged model. """ header = [] - header.append("Number of files averaged : %s\n" % len(self.inputfiles)) + header.append(f"Number of files averaged : {len(self.inputfiles)}\n") for i in self.inputfiles: header.append(i + "\n") - header.append("Total number of dots in the grid : %s\n" % self.grid.shape[0]) + header.append(f"Total number of dots in the grid : {self.grid.shape[0]}\n") decade = 1 for i in range(self.grid.shape[0]): @@ -279,9 +279,9 @@ def save_aver(self, filename): for line in self.header: if line.startswith("ATOM"): if nr < self.grid.shape[0] and self.grid[nr, 4] != 0: - coord = "%8.3f%8.3f%8.3f" % tuple(self.grid[nr, 0:3]) - occ = "%6.2f" % self.grid[nr, 3] - contrib = "%2.f" % self.grid[nr, 4] + coord = "{:8.3f}{:8.3f}{:8.3f}".format(*tuple(self.grid[nr, 0:3])) + occ = f"{self.grid[nr, 3]:6.2f}" + contrib = f"{self.grid[nr, 4]:2.0f}" line = ( line[:30] + coord + occ + line[60:66] + contrib + line[68:] ) diff --git a/src/freesas/containers.py b/src/freesas/containers.py index f5fa788..e67b791 100644 --- a/src/freesas/containers.py +++ b/src/freesas/containers.py @@ -134,10 +134,10 @@ def save(self, filename, source=None): f"I₀= {self.I0_avg:.2f}±{self.I0_std:.2f}", ] with open(filename, "wt", encoding="utf-8") as out: - out.write("# %s %s" % (source or filename, "\n")) + out.write("# {} {}".format(source or filename, "\n")) out.writelines(f"# {txt} \n" for txt in res) out.write("\n# r\tp(r)\tsigma_p(r)\n") - out.writelines("%s\t%s\t%s%s" % (r, p, s, "\n") for r, p, s in zip( + out.writelines("{}\t{}\t{}{}".format(r, p, s, "\n") for r, p, s in zip( self.radius.astype(numpy.float32), self.density_avg.astype(numpy.float32), self.density_std.astype(numpy.float32), diff --git a/src/freesas/cormap.py b/src/freesas/cormap.py index 2e8e624..8281fa5 100644 --- a/src/freesas/cormap.py +++ b/src/freesas/cormap.py @@ -2,7 +2,7 @@ __license__ = "MIT" __copyright__ = "2017, ESRF" -from math import log +from math import log2 import numpy @@ -61,7 +61,7 @@ def __call__(self, n, c): delta = 2**n - self.A(n, c) if delta <= 0: return 0 - return 2.0 ** (log(delta, 2) - n) + return 2.0 ** (log2(delta) - n) def probaHeadOrTail(self, n, c): """Calculate the probability of a longest run of head or tails to occur @@ -77,7 +77,7 @@ def probaHeadOrTail(self, n, c): delta = self.B(n, c) - self.B(n, c - 1) if delta <= 0: return 0 - return min(2.0 ** (log(delta, 2.0) - n), 1.0) + return min(2.0 ** (log2(delta) - n), 1.0) def probaLongerRun(self, n, c): """Calculate the probability for the longest run of heads or tails to exceed the observed length @@ -93,7 +93,7 @@ def probaLongerRun(self, n, c): delta = (2**n) - self.B(n, c) if delta <= 0: return 0 - return min(2.0 ** (log(delta, 2.0) - n), 1.0) + return min(2.0 ** (log2(delta) - n), 1.0) LROH = LongestRunOfHeads() diff --git a/src/freesas/fitting.py b/src/freesas/fitting.py index 58a7d94..2476b03 100644 --- a/src/freesas/fitting.py +++ b/src/freesas/fitting.py @@ -94,18 +94,7 @@ def get_guinier_header(linesep: str, output_format: str | None = None) -> str: # pylint: disable=R1705 if output_format == "csv": return ( - ",".join( - ( - "File", - "Rg", - "Rg StDev", - "I(0)", - "I(0) StDev", - "First point", - "Last point", - "Quality,Aggregated", - ) - ) + "File,Rg,Rg StDev,I(0),I(0) StDev,First point,Last point,Quality,Aggregated" + linesep ) else: @@ -120,13 +109,7 @@ def get_dnn_header(linesep: str, output_format: str | None = None) -> str: # pylint: disable=R1705 if output_format == "csv": return ( - ",".join( - ( - "File", - "Rg", - "Dmax", - ) - ) + "File,Rg,Dmax" + linesep ) else: diff --git a/src/freesas/invariants.py b/src/freesas/invariants.py index 3372843..0b46f3c 100644 --- a/src/freesas/invariants.py +++ b/src/freesas/invariants.py @@ -85,7 +85,7 @@ def calc_Porod(data, guinier): :param Guinier: result of a Guinier fit (instance of RT_RESULT) :return: Volume calculated according to Porrod's formula """ - q, intensity, dI = extrapolate(data, guinier).T + q, intensity, _dI = extrapolate(data, guinier).T denom = trapezoid(intensity * q**2, q) volume = 2 * numpy.pi**2 * guinier.I0 / denom diff --git a/src/freesas/model.py b/src/freesas/model.py index 8a6e486..e8f210e 100644 --- a/src/freesas/model.py +++ b/src/freesas/model.py @@ -105,7 +105,7 @@ def save(self, filename): if nr < self.atoms.shape[0]: line = ( line[:30] - + "%8.3f%8.3f%8.3f" % tuple(self.atoms[nr]) + + "{:8.3f}{:8.3f}{:8.3f}".format(*tuple(self.atoms[nr])) + line[54:] ) else: diff --git a/src/freesas/nexus_parser.py b/src/freesas/nexus_parser.py index 6a90405..c47f2cf 100644 --- a/src/freesas/nexus_parser.py +++ b/src/freesas/nexus_parser.py @@ -138,9 +138,8 @@ def visit_item(self, name, obj): self.skip.add(posixpath.join(name, key, sub)) else: node[path[-1]] = {} - if isinstance(obj, h5py.Dataset): - if len(obj.shape) <= 1: - node[path[-1]] = obj[()] + if isinstance(obj, h5py.Dataset) and len(obj.shape) <= 1: + node[path[-1]] = obj[()] def save(self, filename): with zipfile.ZipFile(filename, "w") as z: diff --git a/src/freesas/plot.py b/src/freesas/plot.py index bfa1c0c..311a215 100644 --- a/src/freesas/plot.py +++ b/src/freesas/plot.py @@ -109,7 +109,7 @@ def scatter_plot( ) else: ax.plot(q, intensity, label=label_exp, color=exp_color, alpha=0.5) - label_guinier += ": $R_g=$%.2f %s, $I_0=$%.2f" % (rg, unit, I0) + label_guinier += f": $R_g=${rg:.2f} {unit}, $I_0=${I0:.2f}" ax.plot( q_guinier, I_guinier, @@ -132,17 +132,11 @@ def scatter_plot( T = numpy.outer(q_ext, r / numpy.pi) T = (4 * numpy.pi * (r[-1] - r[0]) / (len(r) - 1)) * numpy.sinc(T) p = stats.density_avg - label_ift += ": $D_{max}=$%.2f %s,\n $R_g=$%.2f %s, $I_0=$%.2f" % ( - stats.Dmax_avg, - unit, - stats.Rg_avg, - unit, - stats.I0_avg, - ) + label_ift += f": $D_{{max}}=${stats.Dmax_avg:.2f} {unit},\n $R_g=${stats.Rg_avg:.2f} {unit}, $I_0=${stats.I0_avg:.2f}" ax.plot(q_ext, T.dot(p), label=label_ift, color=ift_color) ax.set_ylabel("$I(q)$ (log scale)", fontsize=fontsize) - ax.set_xlabel("$q$ (%s$^{-1}$)" % unit, fontsize=fontsize) + ax.set_xlabel(f"$q$ ({unit}$^{{-1}}$)", fontsize=fontsize) ax.set_title(title) ax.set_yscale("log") # ax.set_ylim(ymin=I.min() * 10, top=I.max() * 1.1) @@ -353,12 +347,12 @@ def guinier_plot( ax.plot( q2[:end], intercept + slope * q2[:end], - label="ln[$I(q)$] = %.2f %.2f * $q^2$" % (intercept, slope), + label=f"ln[$I(q)$] = {intercept:.2f} {slope:.2f} * $q^2$", color="crimson", ) ax.set_ylabel("ln[$I(q)$]", fontsize=fontsize) - ax.set_xlabel("$q^2$ (%s$^{-2}$)" % unit, fontsize=fontsize) - ax.set_title("Guinier plot: $R_{g}=$%.2f %s $I_{0}=$%.2f" % (Rg, unit, I0)) + ax.set_xlabel(f"$q^2$ ({unit}$^{{-2}}$)", fontsize=fontsize) + ax.set_title(f"Guinier plot: $R_{{g}}=${Rg:.2f} {unit} $I_{{0}}=${I0:.2f}") ax.legend() ax.tick_params(axis="x", labelsize=labelsize) ax.tick_params(axis="y", labelsize=labelsize) @@ -407,21 +401,13 @@ def density_plot( ift.radius, ift.density_avg, ift.density_std, - label="BIFT: χ$_{r}^{2}=$%.2f\n $D_{max}=$%.2f %s\n $R_{g}=$%.2f %s\n $I_{0}=$%.2f" - % ( - stats.chi2r_avg, - stats.Dmax_avg, - unit, - stats.Rg_avg, - unit, - stats.I0_avg, - ), + label=f"BIFT: χ$_{{r}}^{{2}}=${stats.chi2r_avg:.2f}\n $D_{{max}}=${stats.Dmax_avg:.2f} {unit}\n $R_{{g}}=${stats.Rg_avg:.2f} {unit}\n $I_{{0}}=${stats.I0_avg:.2f}", capsize=0, color="blue", ecolor="lightblue", ) ax.set_ylabel("$p(r)$", fontsize=fontsize) - ax.set_xlabel("$r$ (%s)" % unit, fontsize=fontsize) + ax.set_xlabel(f"$r$ ({unit})", fontsize=fontsize) ax.set_title("Pair distribution function") ax.legend() ax.tick_params(axis="x", labelsize=labelsize) diff --git a/src/freesas/resources/__init__.py b/src/freesas/resources/__init__.py index 6525533..6c9dc1e 100644 --- a/src/freesas/resources/__init__.py +++ b/src/freesas/resources/__init__.py @@ -67,15 +67,7 @@ # importlib_resources is useful when this package is stored in a zip # When importlib.resources is not available, the resources dir defaults to the # directory containing this module. -if sys.version_info >= (3, 9): - import importlib.resources as importlib_resources -else: - try: - import importlib_resources - except ImportError: - logger.info("Unable to import importlib_resources") - logger.debug("Backtrace", exc_info=True) - importlib_resources = None +import importlib.resources as importlib_resources if importlib_resources is not None: import atexit diff --git a/src/freesas/sas_argparser.py b/src/freesas/sas_argparser.py index 7bb6179..3e5e265 100644 --- a/src/freesas/sas_argparser.py +++ b/src/freesas/sas_argparser.py @@ -48,12 +48,8 @@ def __init__(self, prog: str, description: str, epilog: str, **kwargs): :param kwargs: additional kwargs for argparse ArgumentParser """ - self.usage = "%s [OPTIONS] FILES " % (prog) - version = "%s version %s from %s" % ( - prog, - freesas_version.version, - freesas_version.date, - ) + self.usage = f"{prog} [OPTIONS] FILES " + version = f"{prog} version {freesas_version.version} from {freesas_version.date}" self.parser = argparse.ArgumentParser( usage=self.usage, description=description, epilog=epilog, **kwargs @@ -109,7 +105,7 @@ def add_output_filename_argument(self): type=Path, ) - def add_output_data_format(self, *formats: str, default: str = None): + def add_output_data_format(self, *formats: str, default: str | None = None): """Add default argument for specifying output format.""" help_string = "Output format: " + ", ".join(formats) self.add_argument( diff --git a/src/freesas/test/mock_open_38.py b/src/freesas/test/mock_open_38.py index 53a88c4..7c68e85 100644 --- a/src/freesas/test/mock_open_38.py +++ b/src/freesas/test/mock_open_38.py @@ -54,8 +54,7 @@ def _iter_side_effect(): if handle.readline.return_value is not None: while True: yield handle.readline.return_value - for line in _state[0]: - yield line + yield from _state[0] def _next_side_effect(): if handle.readline.return_value is not None: diff --git a/src/freesas/test/test_align.py b/src/freesas/test/test_align.py index 5c840de..3ad0ee2 100644 --- a/src/freesas/test/test_align.py +++ b/src/freesas/test/test_align.py @@ -71,7 +71,7 @@ def test_alignment(self): logger.error(m.dist(n, m.atoms, n.atoms)) logger.error("pb of movement") dist = align.alignment_2models(save=False) - self.assertAlmostEqual(dist, 0, 12, msg="NSD unequal 0, %s!=0" % dist) + self.assertAlmostEqual(dist, 0, 12, msg=f"NSD unequal 0, {dist}!=0") def test_usefull_alignment(self): inputfiles = [self.testfile1, self.testfile2] @@ -85,7 +85,7 @@ def test_usefull_alignment(self): self.assertGreaterEqual( dist_before, dist_after, - "increase of distance after alignment %s<%s" % (dist_before, dist_after), + f"increase of distance after alignment {dist_before}<{dist_after}", ) def test_optimisation_align(self): @@ -103,8 +103,7 @@ def test_optimisation_align(self): self.assertGreaterEqual( dist_before, dist_after, - "increase of distance after optimized alignment %s<%s" - % (dist_before, dist_after), + f"increase of distance after optimized alignment {dist_before}<{dist_after}", ) def test_alignment_intruder(self): @@ -131,7 +130,7 @@ def test_alignment_intruder(self): self.assertEqual( num_intr, intruder, - msg="not find the good intruder, %s!=%s" % (num_intr, intruder), + msg=f"not find the good intruder, {num_intr}!={intruder}", ) def test_reference(self): diff --git a/src/freesas/test/test_autorg.py b/src/freesas/test/test_autorg.py index a78dd91..a108d54 100644 --- a/src/freesas/test/test_autorg.py +++ b/src/freesas/test/test_autorg.py @@ -87,11 +87,11 @@ def test_atsas(self): logger.info("test file: %s", self.testfile) data = numpy.loadtxt(self.testfile) atsas_result = self.atsas_autorg.copy() - logger.debug("Reference version: %s" % atsas_result.pop("Version")) + logger.debug("Reference version: {}".format(atsas_result.pop("Version"))) atsas_result = RG_RESULT(**atsas_result) free_result = autoRg(data) - logger.debug("Ref: %s" % (atsas_result,)) - logger.debug("Obt: %s" % (free_result,)) + logger.debug(f"Ref: {atsas_result}") + logger.debug(f"Obt: {free_result}") self.assertAlmostEqual( atsas_result.Rg, free_result.Rg, 1, "RG fits within 2 digits" ) diff --git a/src/freesas/test/test_average.py b/src/freesas/test/test_average.py index e21056a..23be1f2 100644 --- a/src/freesas/test/test_average.py +++ b/src/freesas/test/test_average.py @@ -49,8 +49,7 @@ def test_knots(self): self.assertGreater( threshold, gap, - msg="final number of knots too different of wanted number: %s != %s" - % (nbknots, grid.nbknots), + msg=f"final number of knots too different of wanted number: {nbknots} != {grid.nbknots}", ) def test_makegrid(self): diff --git a/src/freesas/test/test_bift.py b/src/freesas/test/test_bift.py index 2e8cfaf..f4a06eb 100644 --- a/src/freesas/test/test_bift.py +++ b/src/freesas/test/test_bift.py @@ -82,7 +82,7 @@ def test_autobift(self): data = numpy.vstack((self.q, self.I, self.err)).T t0 = time.perf_counter() bo = auto_bift(data) - key, value, valid = bo.get_best() + key, _value, _valid = bo.get_best() # print("key is ", key) stats = bo.calc_stats() # print("stat is ", stats) diff --git a/src/freesas/test/test_distance.py b/src/freesas/test/test_distance.py index 0bfe566..74c4166 100644 --- a/src/freesas/test/test_distance.py +++ b/src/freesas/test/test_distance.py @@ -27,10 +27,10 @@ def test_invariants(self): f_np, r_np, d_np = m.calc_invariants(False) f_cy, r_cy, d_cy = m.calc_invariants(True) self.assertAlmostEqual( - f_np, f_cy, 10, "fineness is the same %s!=%s" % (f_np, f_cy) + f_np, f_cy, 10, f"fineness is the same {f_np}!={f_cy}" ) - self.assertAlmostEqual(r_np, r_cy, 10, "Rg is the same %s!=%s" % (r_np, r_cy)) - self.assertAlmostEqual(d_np, d_cy, 10, "Dmax is the same %s!=%s" % (d_np, d_cy)) + self.assertAlmostEqual(r_np, r_cy, 10, f"Rg is the same {r_np}!={r_cy}") + self.assertAlmostEqual(d_np, d_cy, 10, f"Dmax is the same {d_np}!={d_cy}") def test_distance(self): m = SASModel() @@ -40,7 +40,7 @@ def test_distance(self): f_np = m.dist(n, m.atoms, n.atoms, False) f_cy = m.dist(n, m.atoms, n.atoms, True) self.assertAlmostEqual( - f_np, f_cy, 5, "distance is the same %s!=%s" % (f_np, f_cy) + f_np, f_cy, 5, f"distance is the same {f_np}!={f_cy}" ) def test_same(self): diff --git a/src/freesas/test/test_dnn.py b/src/freesas/test/test_dnn.py index aeb6b14..a082ed7 100644 --- a/src/freesas/test/test_dnn.py +++ b/src/freesas/test/test_dnn.py @@ -82,7 +82,7 @@ def test_preprocess(self): """ datfile = get_datafile("bsa_005_sub.dat") data = load_scattering_data(datfile) - q, intensity, sigma = data.T + q, intensity, _sigma = data.T Iprep = preprocess(q, intensity) self.assertEqual(Iprep.max(), 1, msg="range 0-1") self.assertEqual(Iprep.shape, (1024,), msg="size 1024") diff --git a/src/freesas/test/test_model.py b/src/freesas/test/test_model.py index 8951846..63aacc5 100644 --- a/src/freesas/test/test_model.py +++ b/src/freesas/test/test_model.py @@ -65,7 +65,7 @@ def test_rfactor(self): self.assertEqual( m.rfactor, n.rfactor, - msg="R-factor is not the same %s != %s" % (m.rfactor, n.rfactor), + msg=f"R-factor is not the same {m.rfactor} != {n.rfactor}", ) def test_init(self): @@ -81,8 +81,7 @@ def test_centroid(self): m.centroid() if len(m.com) != 3: logger.error( - "center of mass has not been saved correctly : length of COM position vector = %s!=3" - % (len(m.com)) + f"center of mass has not been saved correctly : length of COM position vector = {len(m.com)}!=3" ) mol_centered = m.atoms[:, 0:3] - m.com center = mol_centered.mean(axis=0) @@ -91,8 +90,7 @@ def test_centroid(self): norm, 0, 12, - msg="molecule is not centered : norm of the COM position vector %s!=0" - % (norm), + msg=f"molecule is not centered : norm of the COM position vector {norm}!=0", ) def test_inertia_tensor(self): @@ -100,15 +98,14 @@ def test_inertia_tensor(self): m.inertiatensor() tensor = m.inertensor assert tensor.shape == (3, 3), ( - "inertia tensor has not been saved correctly : shape of inertia matrix = %s" - % (tensor.shape) + f"inertia tensor has not been saved correctly : shape of inertia matrix = {tensor.shape}" ) def test_canonical_translate(self): m = assign_random_mol() trans = m.canonical_translate() if trans.shape != (4, 4): - logger.error("pb with translation matrix shape: shape=%s" % (trans.shape)) + logger.error(f"pb with translation matrix shape: shape={trans.shape}") com = m.com com_componants = [com[0], com[1], com[2]] trans_vect = [-trans[0, -1], -trans[1, -1], -trans[2, -1]] @@ -125,7 +122,7 @@ def test_canonical_rotate(self): logger.error("enantiomer has not been selected") det = numpy.linalg.det(rot) self.assertAlmostEqual( - det, 1, 10, msg="rotation matrix determinant is not 1: %s" % (det) + det, 1, 10, msg=f"rotation matrix determinant is not 1: {det}" ) def test_canonical_parameters(self): @@ -152,7 +149,7 @@ def test_dist(self): m = assign_random_mol() n = SASModel(m.atoms) distance = m.dist(n, m.atoms, n.atoms) - self.assertEqual(distance, 0, msg="NSD different of 0: %s!=0" % (distance)) + self.assertEqual(distance, 0, msg=f"NSD different of 0: {distance}!=0") def test_can_transform(self): m = assign_random_mol() @@ -183,7 +180,7 @@ def test_dist_move(self): p0 = m.can_param dist_after_mvt = m.dist_after_movement(p0, n, [1, 1, 1]) self.assertEqual( - dist_after_mvt, 0, msg="NSD different of 0: %s!=0" % (dist_after_mvt) + dist_after_mvt, 0, msg=f"NSD different of 0: {dist_after_mvt}!=0" ) def test_reverse_transform(self): @@ -194,7 +191,7 @@ def test_reverse_transform(self): m.atoms = m.transform(m.can_param, [1, 1, 1], reverse=True) dist = m.dist(n, m.atoms, n.atoms) self.assertAlmostEqual( - dist, 0.0, 10, msg="pb with reverse transformation : %s != 0.0" % dist + dist, 0.0, 10, msg=f"pb with reverse transformation : {dist} != 0.0" ) diff --git a/src/freesas/transformations.py b/src/freesas/transformations.py index 8e16d37..2b584ef 100644 --- a/src/freesas/transformations.py +++ b/src/freesas/transformations.py @@ -698,7 +698,7 @@ def shear_from_matrix(matrix): w, V = numpy.linalg.eig(M33) i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-4)[0] if len(i) < 2: - raise ValueError("no two linear independent eigenvectors found %s" % w) + raise ValueError(f"no two linear independent eigenvectors found {w}") V = numpy.real(V[:, i]).squeeze().T lenorm = -1.0 for i0, i1 in ((0, 1), (0, 2), (1, 2)): @@ -1711,7 +1711,7 @@ def arcball_nearest_axis(point, axes): "rzyz": (2, 1, 1, 1), } -_TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items()) +_TUPLE2AXES = {v: k for k, v in _AXES2TUPLE.items()} def vector_norm(data, axis=None, out=None): @@ -1931,7 +1931,7 @@ def _import_module(name, package=None, warn=True, prefix="_py_", ignore="_"): module = import_module("." + name, package=package) except ImportError: if warn: - warnings.warn("failed to import module %s" % name) + warnings.warn(f"failed to import module {name}") else: for attr in dir(module): if ignore and attr.startswith(ignore): From 976754dfe745a3cf4cc49b9eb7758eb43860b641 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Wed, 2 Sep 2026 08:36:28 +0200 Subject: [PATCH 5/9] fix ruff spotted errors --- src/freesas/__init__.py | 2 +- src/freesas/app/auto_gpa.py | 1 - src/freesas/app/auto_guinier.py | 1 - src/freesas/app/autorg.py | 1 - src/freesas/app/bift.py | 21 +++- src/freesas/app/cormap.py | 8 +- src/freesas/app/dnn.py | 1 - src/freesas/app/extract_ascii.py | 1 - src/freesas/app/plot_sas.py | 1 - src/freesas/app/supycomb.py | 3 +- src/freesas/average.py | 8 +- src/freesas/model.py | 4 +- src/freesas/plot.py | 23 +--- src/freesas/sasio.py | 4 +- src/freesas/test/__init__.py | 2 - src/freesas/test/meson.build | 1 - src/freesas/test/mock_open_38.py | 98 ---------------- src/freesas/test/test_align.py | 2 - src/freesas/test/test_all.py | 2 - src/freesas/test/test_autorg.py | 3 +- src/freesas/test/test_average.py | 5 +- src/freesas/test/test_cormap.py | 2 - src/freesas/test/test_distance.py | 2 - src/freesas/test/test_fitting.py | 151 +++++++++++++------------ src/freesas/test/test_model.py | 2 - src/freesas/test/test_sas_argparser.py | 2 - src/freesas/test/utilstest.py | 2 - version.py | 62 ++++++---- 28 files changed, 152 insertions(+), 263 deletions(-) delete mode 100644 src/freesas/test/mock_open_38.py diff --git a/src/freesas/__init__.py b/src/freesas/__init__.py index 0f67edf..73a60e6 100644 --- a/src/freesas/__init__.py +++ b/src/freesas/__init__.py @@ -21,7 +21,7 @@ # THE SOFTWARE. # # ###########################################################################*/ -""" """ +"""Small angle scattering tools, free and written in Python.""" __authors__ = ["Jérôme Kieffer"] __license__ = "MIT" diff --git a/src/freesas/app/auto_gpa.py b/src/freesas/app/auto_gpa.py index f24c202..a5ee36c 100644 --- a/src/freesas/app/auto_gpa.py +++ b/src/freesas/app/auto_gpa.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # # Project: freesas # https://github.com/kif/freesas diff --git a/src/freesas/app/auto_guinier.py b/src/freesas/app/auto_guinier.py index 1ba92f6..aea9ee6 100644 --- a/src/freesas/app/auto_guinier.py +++ b/src/freesas/app/auto_guinier.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # # Project: freesas # https://github.com/kif/freesas diff --git a/src/freesas/app/autorg.py b/src/freesas/app/autorg.py index a05a66e..38e9eba 100644 --- a/src/freesas/app/autorg.py +++ b/src/freesas/app/autorg.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # # Project: freesas # https://github.com/kif/freesas diff --git a/src/freesas/app/bift.py b/src/freesas/app/bift.py index d8ef40f..3803e3e 100644 --- a/src/freesas/app/bift.py +++ b/src/freesas/app/bift.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # # Project: freesas # https://github.com/kif/freesas @@ -28,12 +27,17 @@ __copyright__ = "2017-2026, ESRF" __date__ = "01/09/2026" +import io import logging import platform import sys import traceback from freesas import bift +from freesas.autorg import ( + InsufficientDataError, + NoGuinierRegionError, +) from freesas.fitting import ( collect_files, set_logging_level, @@ -106,7 +110,9 @@ def build_parser() -> SASParser: def main(): """Entry point for bift app.""" if platform.system() == "Windows": - sys.stdout = open(1, "w", encoding="utf-16", closefd=False) + sys.stdout = io.TextIOWrapper( + sys.stdout.buffer, encoding="utf-16", write_through=True + ) parser = build_parser() args = parser.parse_args() @@ -116,14 +122,21 @@ def main(): for afile in files: try: data = load_scattering_data(afile) - except Exception: + except OSError: + logger.error("Unable to read file %s", afile) + except ValueError: logger.error("Unable to parse file %s", afile) else: if args.unit == "Å": data = convert_inverse_angstrom_to_nanometer(data) try: bo = bift.auto_bift(data, npt=args.npt, scan_size=args.scan) - except Exception as err: + except ( + InsufficientDataError, + NoGuinierRegionError, + ValueError, + IndexError, + ) as err: print(f"{afile}: {err.__class__.__name__} {err}") if logging.root.level < logging.WARNING: traceback.print_exc(file=sys.stdout) diff --git a/src/freesas/app/cormap.py b/src/freesas/app/cormap.py index 77ff229..4ef6a3c 100644 --- a/src/freesas/app/cormap.py +++ b/src/freesas/app/cormap.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - __author__ = "Jérôme Kieffer" __license__ = "MIT" __copyright__ = "2015, ESRF" @@ -67,10 +65,12 @@ def compare(lstfiles): data.append(d) for a, b in combinations(data, 2): r = gof(a.data, b.data) - res.append("%6i vs. %6i %6i %8.6f" % (a.index, b.index, r.c, r.P)) + res.append( + f"{a.index:6d} vs. {b.index:6d} {r.c:6d} {r.P:8.6f}" + ) res.append("") for a in data: - res.append("%6i %8f + %8f * %s" % (a.index, 0.0, 1.0, a.filename)) + res.append(f"{a.index:6d} {0.0:8f} + {1.0:8f} * {a.filename}") res.append("") print(os.linesep.join(res)) return res diff --git a/src/freesas/app/dnn.py b/src/freesas/app/dnn.py index 8157cfd..d7f6e55 100644 --- a/src/freesas/app/dnn.py +++ b/src/freesas/app/dnn.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # # Project: freesas # https://github.com/kif/freesas diff --git a/src/freesas/app/extract_ascii.py b/src/freesas/app/extract_ascii.py index e81e33b..2fa9ed3 100644 --- a/src/freesas/app/extract_ascii.py +++ b/src/freesas/app/extract_ascii.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # # Project: freesas # https://github.com/kif/freesas diff --git a/src/freesas/app/plot_sas.py b/src/freesas/app/plot_sas.py index 1c9b99f..5bc2d16 100644 --- a/src/freesas/app/plot_sas.py +++ b/src/freesas/app/plot_sas.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 # # Project: freesas # https://github.com/kif/freesas diff --git a/src/freesas/app/supycomb.py b/src/freesas/app/supycomb.py index fec7f25..dbae8ef 100644 --- a/src/freesas/app/supycomb.py +++ b/src/freesas/app/supycomb.py @@ -1,4 +1,3 @@ -#!/usr/bin/python3 __author__ = "Guillaume Bonamis" __license__ = "MIT" __copyright__ = "2015, ESRF" @@ -113,7 +112,7 @@ def main(): logger.info(f"{args.file[0]} and {args.file[1]} aligned") logger.info(f"NSD after optimized alignment = {dist:.2f}") else: - align.outputfiles = ["model-%02i.pdb" % (i + 1) for i in range(input_len)] + align.outputfiles = [f"model-{i + 1:02d}.pdb" for i in range(input_len)] selection.inputfiles = args.file selection.models_selection() selection.rfactorplot(save=save) diff --git a/src/freesas/average.py b/src/freesas/average.py index 186bb8d..e9db53c 100644 --- a/src/freesas/average.py +++ b/src/freesas/average.py @@ -23,7 +23,7 @@ def __init__(self, inputfiles): self.coordknots = [] def __repr__(self): - return "Grid with %i knots" % self.nbknots + return f"Grid with {self.nbknots} knots" def spatial_extent(self): """ @@ -172,7 +172,7 @@ def __init__(self, inputfiles, grid): self.grid = grid def __repr__(self): - return "Average SAS model with %i atoms" % len(self.atoms) + return f"Average SAS model with {len(self.atoms)} atoms" def read_files(self, reference=None): """ @@ -255,10 +255,10 @@ def make_header(self): decade = 1 for i in range(self.grid.shape[0]): line = "ATOM CA ASP 1 20.00 2 201\n" - line = line[:7] + "%4.i" % (i + 1) + line[11:] + line = line[:7] + f"{i + 1:4d}" + line[11:] if not (i + 1) % 10: decade += 1 - line = line[:21] + "%4.i" % decade + line[25:] + line = line[:21] + f"{decade:4d}" + line[25:] header.append(line) self.header = header return header diff --git a/src/freesas/model.py b/src/freesas/model.py index e8f210e..a911247 100644 --- a/src/freesas/model.py +++ b/src/freesas/model.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - __author__ = "Guillaume Bonamis" __license__ = "MIT" __copyright__ = "2015, ESRF" @@ -61,7 +59,7 @@ def __init__(self, molecule=None): self._sem = threading.Semaphore() def __repr__(self): - return "SAS model with %i atoms" % len(self.atoms) + return f"SAS model with {len(self.atoms)} atoms" def read(self, filename): """ diff --git a/src/freesas/plot.py b/src/freesas/plot.py index 311a215..172aaf6 100644 --- a/src/freesas/plot.py +++ b/src/freesas/plot.py @@ -52,7 +52,7 @@ def scatter_plot( intensity = data.T[1] try: err = data.T[2] - except Exception: + except IndexError: err = None if ax: fig = ax.figure @@ -148,7 +148,7 @@ def scatter_plot( for lbl in [label_exp, label_guinier, label_ift]: try: idx = lab.index(lbl) - except Exception: + except ValueError: continue ordered_lab.append(lab[idx]) ordered_crv.append(crv[idx]) @@ -192,7 +192,7 @@ def kratky_plot( intensity = data.T[1] try: err = data.T[2] - except Exception: + except IndexError: err = None if ax: fig = ax.figure @@ -431,21 +431,10 @@ def plot_all( ): from . import autorg, bift - try: - guinier = autorg.autoRg(data) - except autorg.InsufficientDataError: - raise + guinier = autorg.autoRg(data) logger.debug(guinier) - try: - bo = bift.auto_bift(data, npt=100, scan_size=11, Dmax_over_Rg=3) - except ( - autorg.InsufficientDataError, - autorg.NoGuinierRegionError, - ValueError, - ): - raise - else: - ift = bo.calc_stats() + bo = bift.auto_bift(data, npt=100, scan_size=11, Dmax_over_Rg=3) + ift = bo.calc_stats() logger.debug(ift) fig, ax = subplots(2, 2, figsize=(12, 10)) scatter_plot( diff --git a/src/freesas/sasio.py b/src/freesas/sasio.py index 60b9bcf..3a4b8e2 100644 --- a/src/freesas/sasio.py +++ b/src/freesas/sasio.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # # Project: FreeSAS # https://github.com/kif/freesas @@ -22,11 +21,10 @@ import io from os import PathLike -from typing import Union from numpy import array, loadtxt, ndarray -PathType = Union[PathLike, str, bytes, io.StringIO, io.BytesIO] +PathType = PathLike | str | bytes | io.StringIO | io.BytesIO def load_scattering_data(filename: PathType) -> ndarray: diff --git a/src/freesas/test/__init__.py b/src/freesas/test/__init__.py index edf7cba..c792ffb 100644 --- a/src/freesas/test/__init__.py +++ b/src/freesas/test/__init__.py @@ -1,5 +1,3 @@ -#!usr/bin/env python - __author__ = "Jérôme Kieffer" __license__ = "MIT" __date__ = "15/01/2021" diff --git a/src/freesas/test/meson.build b/src/freesas/test/meson.build index 3b82001..5d5e79b 100644 --- a/src/freesas/test/meson.build +++ b/src/freesas/test/meson.build @@ -1,6 +1,5 @@ py.install_sources([ '__init__.py', -'mock_open_38.py', 'test_align.py', 'test_all.py', 'test_autorg.py', diff --git a/src/freesas/test/mock_open_38.py b/src/freesas/test/mock_open_38.py deleted file mode 100644 index 7c68e85..0000000 --- a/src/freesas/test/mock_open_38.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -This is the Python 3.8 implementation of mock_open taken from -https://github.com/python/cpython/blob/3.8/Lib/unittest/mock.py -Hence: -"Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; -All Rights Reserved" -""" - -import io -from unittest.mock import DEFAULT, MagicMock - -file_spec = None -# sentinel = _Sentinel() -# DEFAULT = sentinel.DEFAULT - - -def _to_stream(read_data): - if isinstance(read_data, bytes): - return io.BytesIO(read_data) - else: - return io.StringIO(read_data) - - -def mock_open(mock=None, read_data=""): - """ - A helper function to create a mock to replace the use of `open`. It works - for `open` called directly or used as a context manager. - The `mock` argument is the mock object to configure. If `None` (the - default) then a `MagicMock` will be created for you, with the API limited - to methods or attributes available on standard file handles. - `read_data` is a string for the `read`, `readline` and `readlines` of the - file handle to return. This is an empty string by default. - """ - _read_data = _to_stream(read_data) - _state = [_read_data, None] - - def _readlines_side_effect(*args, **kwargs): - if handle.readlines.return_value is not None: - return handle.readlines.return_value - return _state[0].readlines(*args, **kwargs) - - def _read_side_effect(*args, **kwargs): - if handle.read.return_value is not None: - return handle.read.return_value - return _state[0].read(*args, **kwargs) - - def _readline_side_effect(*args, **kwargs): - yield from _iter_side_effect() - while True: - yield _state[0].readline(*args, **kwargs) - - def _iter_side_effect(): - if handle.readline.return_value is not None: - while True: - yield handle.readline.return_value - yield from _state[0] - - def _next_side_effect(): - if handle.readline.return_value is not None: - return handle.readline.return_value - return next(_state[0]) - - global file_spec - if file_spec is None: - import _io - - file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) - - if mock is None: - mock = MagicMock(name="open", spec=open) - - handle = MagicMock(spec=file_spec) - handle.__enter__.return_value = handle - - handle.write.return_value = None - handle.read.return_value = None - handle.readline.return_value = None - handle.readlines.return_value = None - - handle.read.side_effect = _read_side_effect - _state[1] = _readline_side_effect() - handle.readline.side_effect = _state[1] - handle.readlines.side_effect = _readlines_side_effect - handle.__iter__.side_effect = _iter_side_effect - handle.__next__.side_effect = _next_side_effect - - def reset_data(*args, **kwargs): - _state[0] = _to_stream(read_data) - if handle.readline.side_effect == _state[1]: - # Only reset the side effect if the user hasn't overridden it. - _state[1] = _readline_side_effect() - handle.readline.side_effect = _state[1] - return DEFAULT - - mock.side_effect = reset_data - mock.return_value = handle - return mock diff --git a/src/freesas/test/test_align.py b/src/freesas/test/test_align.py index 3ad0ee2..34aa4a4 100644 --- a/src/freesas/test/test_align.py +++ b/src/freesas/test/test_align.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - __author__ = "Guillaume" __license__ = "MIT" __copyright__ = "2015, ESRF" diff --git a/src/freesas/test/test_all.py b/src/freesas/test/test_all.py index 539a83e..5b9357c 100644 --- a/src/freesas/test/test_all.py +++ b/src/freesas/test/test_all.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - __author__ = "Guillaume" __license__ = "MIT" __copyright__ = "2015, ESRF" diff --git a/src/freesas/test/test_autorg.py b/src/freesas/test/test_autorg.py index a108d54..a8b5576 100644 --- a/src/freesas/test/test_autorg.py +++ b/src/freesas/test/test_autorg.py @@ -29,6 +29,7 @@ import logging import unittest from math import pi, sqrt +from typing import ClassVar import numpy from scipy.stats import linregress @@ -71,7 +72,7 @@ def __init__(self, testName, **extra_kwargs): self.extra_arg = extra_kwargs # Reference implementation - atsas_autorg = { + atsas_autorg: ClassVar = { "Version": "Atsas 2.6.1", "Rg": 2.98016, "sigma_Rg": 0.156859, diff --git a/src/freesas/test/test_average.py b/src/freesas/test/test_average.py index 23be1f2..cf51eaa 100644 --- a/src/freesas/test/test_average.py +++ b/src/freesas/test/test_average.py @@ -1,11 +1,10 @@ -#!/usr/bin/python - __author__ = "Guillaume" __license__ = "MIT" __copyright__ = "2015, ESRF" import logging import unittest +from typing import ClassVar import numpy @@ -20,7 +19,7 @@ class TestAverage(unittest.TestCase): testfile1 = get_datafile("model-01.pdb") testfile2 = get_datafile("model-02.pdb") - inputfiles = [testfile1, testfile2] + inputfiles: ClassVar = [testfile1, testfile2] grid = Grid(inputfiles) def test_gridsize(self): diff --git a/src/freesas/test/test_cormap.py b/src/freesas/test/test_cormap.py index b0cf7da..e6d1251 100644 --- a/src/freesas/test/test_cormap.py +++ b/src/freesas/test/test_cormap.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - __author__ = "Jérôme Kieffer" __license__ = "MIT" __copyright__ = "2017-2026, ESRF" diff --git a/src/freesas/test/test_distance.py b/src/freesas/test/test_distance.py index 74c4166..2db394f 100644 --- a/src/freesas/test/test_distance.py +++ b/src/freesas/test/test_distance.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - __author__ = "Jérôme Kieffer" __license__ = "MIT" __copyright__ = "2015, ESRF" diff --git a/src/freesas/test/test_fitting.py b/src/freesas/test/test_fitting.py index d0a28e9..28afd62 100644 --- a/src/freesas/test/test_fitting.py +++ b/src/freesas/test/test_fitting.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - """Test the functionality of fitting module.""" __authors__ = ["Martha Brennich"] @@ -15,10 +13,9 @@ import sys import unittest from collections.abc import Callable -from errno import ENOENT from io import StringIO from types import SimpleNamespace -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, mock_open, patch import numpy @@ -33,12 +30,6 @@ ) from ..sas_argparser import GuinierParser -if sys.version_info.minor > 6: - from unittest.mock import mock_open -else: - from .mock_open_38 import mock_open - - logger = logging.getLogger(__name__) @@ -237,12 +228,14 @@ def test_get_output_destination_with_path_input_returns_writable_io( """Test that by calling get_output_destination with a Path as input we obtain write access to the file of Path.""" mocked_open = mock_open() - with patch("builtins.open", mocked_open): - with get_output_destination(pathlib.Path("test")) as destination: - self.assertTrue( - destination.writable(), - msg="file destination is writable", - ) + with ( + patch("builtins.open", mocked_open), + get_output_destination(pathlib.Path("test")) as destination, + ): + self.assertTrue( + destination.writable(), + msg="file destination is writable", + ) mocked_open.assert_called_once_with(pathlib.Path("test"), "w") def test_get_output_destination_without_input_returns_stdout( @@ -329,10 +322,7 @@ def os_stat_mock(path, **_): if "good" in pathlib.Path(path).name: pass else: - if sys.version_info.minor > 7: - raise ValueError - else: - raise OSError(ENOENT, "dummy") + raise ValueError mocked_stat = MagicMock(side_effect=os_stat_mock) with patch("os.stat", mocked_stat): @@ -353,24 +343,23 @@ def test_collect_files_globs_on_windows(self): """Test that collect_files globs on Windows if no existent files provided.""" def os_stat_mock(path): - if sys.version_info.minor > 7: - raise ValueError - else: - raise OSError(ENOENT, "dummy") + raise ValueError mocked_stat = MagicMock(side_effect=os_stat_mock) mocked_glob = MagicMock( side_effect=[(p for p in [pathlib.Path("pathA"), pathlib.Path("pathB")])] ) - with patch("os.stat", mocked_stat): - with patch.object(pathlib.Path, "glob", mocked_glob): - fit = importlib.import_module("..fitting", "freesas.subpkg") - fit = importlib.reload(fit) - self.assertEqual( - fit.collect_files(["testgood"]), - [pathlib.Path("pathA"), pathlib.Path("pathB")], - msg="collect_files on windows returns list if fiel argument does not exist", - ) + with ( + patch("os.stat", mocked_stat), + patch.object(pathlib.Path, "glob", mocked_glob), + ): + fit = importlib.import_module("..fitting", "freesas.subpkg") + fit = importlib.reload(fit) + self.assertEqual( + fit.collect_files(["testgood"]), + [pathlib.Path("pathA"), pathlib.Path("pathB")], + msg="collect_files on windows returns list if fiel argument does not exist", + ) mocked_glob.assert_called_once() # Reload without the patch @@ -579,13 +568,15 @@ def dummy_fit_function(input_data: numpy.ndarray) -> RG_RESULT: ) output_catcher_stdout = StringIO() output_catcher_stderr = StringIO() - with contextlib.redirect_stdout(output_catcher_stdout): - with contextlib.redirect_stderr(output_catcher_stderr): - run_guinier_fit( - fit_function=dummy_fit_function, - parser=dummy_parser, - logger=logger, - ) + with ( + contextlib.redirect_stdout(output_catcher_stdout), + contextlib.redirect_stderr(output_catcher_stderr), + ): + run_guinier_fit( + fit_function=dummy_fit_function, + parser=dummy_parser, + logger=logger, + ) expected_stdout_output = ( "test2 Rg=3.1000(±0.1000) I0=103.0000(±2.5000) [13-207] 5010.00% linesep" ) @@ -635,13 +626,15 @@ def dummy_fit_function(input_data: numpy.ndarray) -> RG_RESULT: ) output_catcher_stdout = StringIO() output_catcher_stderr = StringIO() - with contextlib.redirect_stdout(output_catcher_stdout): - with contextlib.redirect_stderr(output_catcher_stderr): - run_guinier_fit( - fit_function=dummy_fit_function, - parser=dummy_parser, - logger=logger, - ) + with ( + contextlib.redirect_stdout(output_catcher_stdout), + contextlib.redirect_stderr(output_catcher_stderr), + ): + run_guinier_fit( + fit_function=dummy_fit_function, + parser=dummy_parser, + logger=logger, + ) expected_stdout_output = ( "test2 Rg=3.1000(±0.1000) I0=103.0000(±2.5000) [13-207] 5010.00% linesep" ) @@ -695,13 +688,15 @@ def dummy_fit_function(input_data: numpy.ndarray) -> RG_RESULT: ) output_catcher_stdout = StringIO() output_catcher_stderr = StringIO() - with contextlib.redirect_stdout(output_catcher_stdout): - with contextlib.redirect_stderr(output_catcher_stderr): - run_guinier_fit( - fit_function=dummy_fit_function, - parser=dummy_parser, - logger=logger, - ) + with ( + contextlib.redirect_stdout(output_catcher_stdout), + contextlib.redirect_stderr(output_catcher_stderr), + ): + run_guinier_fit( + fit_function=dummy_fit_function, + parser=dummy_parser, + logger=logger, + ) expected_stdout_output = ( "test2 Rg=3.1000(±0.1000) I0=103.0000(±2.5000) [13-207] 5010.00% linesep" ) @@ -753,13 +748,15 @@ def dummy_fit_function(input_data: numpy.ndarray) -> RG_RESULT: ) output_catcher_stdout = StringIO() output_catcher_stderr = StringIO() - with contextlib.redirect_stdout(output_catcher_stdout): - with contextlib.redirect_stderr(output_catcher_stderr): - run_guinier_fit( - fit_function=dummy_fit_function, - parser=dummy_parser, - logger=logger, - ) + with ( + contextlib.redirect_stdout(output_catcher_stdout), + contextlib.redirect_stderr(output_catcher_stderr), + ): + run_guinier_fit( + fit_function=dummy_fit_function, + parser=dummy_parser, + logger=logger, + ) expected_stdout_output = ( "test2 Rg=3.1000(±0.1000) I0=103.0000(±2.5000) [13-207] 5010.00% linesep" ) @@ -811,13 +808,15 @@ def dummy_fit_function(input_data: numpy.ndarray) -> RG_RESULT: ) output_catcher_stdout = StringIO() output_catcher_stderr = StringIO() - with contextlib.redirect_stdout(output_catcher_stdout): - with contextlib.redirect_stderr(output_catcher_stderr): - run_guinier_fit( - fit_function=dummy_fit_function, - parser=dummy_parser, - logger=logger, - ) + with ( + contextlib.redirect_stdout(output_catcher_stdout), + contextlib.redirect_stderr(output_catcher_stderr), + ): + run_guinier_fit( + fit_function=dummy_fit_function, + parser=dummy_parser, + logger=logger, + ) expected_stdout_output = ( "test2 Rg=3.1000(±0.1000) I0=103.0000(±2.5000) [13-207] 5010.00% linesep" ) @@ -869,13 +868,15 @@ def dummy_fit_function(input_data: numpy.ndarray) -> RG_RESULT: ) output_catcher_stdout = StringIO() output_catcher_stderr = StringIO() - with contextlib.redirect_stdout(output_catcher_stdout): - with contextlib.redirect_stderr(output_catcher_stderr): - run_guinier_fit( - fit_function=dummy_fit_function, - parser=dummy_parser, - logger=logger, - ) + with ( + contextlib.redirect_stdout(output_catcher_stdout), + contextlib.redirect_stderr(output_catcher_stderr), + ): + run_guinier_fit( + fit_function=dummy_fit_function, + parser=dummy_parser, + logger=logger, + ) expected_stdout_output = ( "test2 Rg=3.1000(±0.1000) I0=103.0000(±2.5000) [13-207] 5010.00% linesep" ) diff --git a/src/freesas/test/test_model.py b/src/freesas/test/test_model.py index 63aacc5..1b0d165 100644 --- a/src/freesas/test/test_model.py +++ b/src/freesas/test/test_model.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - __author__ = "Guillaume" __license__ = "MIT" __copyright__ = "2015, ESRF" diff --git a/src/freesas/test/test_sas_argparser.py b/src/freesas/test/test_sas_argparser.py index 1714800..94fe09c 100644 --- a/src/freesas/test/test_sas_argparser.py +++ b/src/freesas/test/test_sas_argparser.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - """Test the functionality of SASParser and GuinierParser""" __authors__ = ["Martha Brennich"] diff --git a/src/freesas/test/utilstest.py b/src/freesas/test/utilstest.py index 275ff37..ed9374f 100644 --- a/src/freesas/test/utilstest.py +++ b/src/freesas/test/utilstest.py @@ -1,5 +1,3 @@ -#!usr/bin/env python - __author__ = "Jérôme Kieffer" __license__ = "MIT" __date__ = "06/02/2026" diff --git a/version.py b/version.py index af1c4d9..c56eb24 100755 --- a/version.py +++ b/version.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 -# coding: utf-8 -# /*########################################################################## # -# Copyright (c) 2015-2023 European Synchrotron Radiation Facility +# Copyright (c) 2015-2026 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # -# ###########################################################################*/ """Unique place where the version number is defined. provides: @@ -52,15 +49,20 @@ __authors__ = ["Jérôme Kieffer"] __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "11/09/2024" +__date__ = "02/09/2026" __status__ = "production" __docformat__ = 'restructuredtext' -__all__ = ["date", "version_info", "strictversion", "hexversion", "debianversion", - "calc_hexversion", "citation"] - - -from collections import namedtuple - +__all__ = [ + "calc_hexversion", + "citation", + "date", + "debianversion", + "hexversion", + "strictversion", + "version_info", +] + +from typing import NamedTuple RELEASE_LEVEL_VALUE = {"dev": 0, "alpha": 10, @@ -73,26 +75,37 @@ "beta": "b", "candidate": "rc"} MAJOR = 2026 -MINOR = 3 +MINOR = 9 MICRO = 0 -RELEV = "final" # <16 -SERIAL = 0 # <16 - +RELEV = "dev" # <16 +SERIAL = 0 # <16 date = __date__ -_version_info = namedtuple("version_info", ["major", "minor", "micro", "releaselevel", "serial"]) +class _VersionInfo(NamedTuple): + major: int + minor: int + micro: int + releaselevel: str + serial: int -version_info = _version_info(MAJOR, MINOR, MICRO, RELEV, SERIAL) +class _DatedVersion(NamedTuple): + version: str + date: str -strictversion = version = debianversion = "%d.%d.%d" % version_info[:3] +version_info = _VersionInfo(MAJOR, MINOR, MICRO, RELEV, SERIAL) -_dated_version = namedtuple("dated_version", ["version", "date"]) -dated_version = _dated_version(version=version, date=date) +strictversion = version = debianversion = f"{version_info[0]}.{version_info[1]}.{version_info[2]}" if version_info.releaselevel != "final": - version += "-%s%s" % version_info[-2:] - debianversion += "~adev%i" % version_info[-1] if RELEV == "dev" else "~%s%i" % version_info[-2:] - strictversion += PRERELEASE_NORMALIZED_NAME[version_info[3]] + str(version_info[-1]) + _prerelease = PRERELEASE_NORMALIZED_NAME[version_info[3]] + version += f"-{_prerelease}{version_info[-1]}" + debianversion += ( + f"~adev{version_info[-1]}" + if RELEV == "dev" + else f"~{_prerelease}{version_info[-1]}" + ) + strictversion += _prerelease + str(version_info[-1]) +dated_version = _DatedVersion(version=version, date=date) @@ -114,7 +127,7 @@ def calc_hexversion(major=0, minor=0, micro=0, releaselevel="dev", serial=0, str _PATTERN = re.compile(r"(\d+)\.(\d+)\.(\d+)(\w+)?$") result = _PATTERN.match(string) if result is None: - raise ValueError("'%s' is not a valid version" % string) + raise ValueError(f"'{string}' is not a valid version") result = result.groups() major, minor, micro = int(result[0]), int(result[1]), int(result[2]) releaselevel = result[3] @@ -135,7 +148,6 @@ def calc_hexversion(major=0, minor=0, micro=0, releaselevel="dev", serial=0, str hexversion = calc_hexversion(*version_info) - citation = "doi:10.1107/S1600577522007238" if __name__ == "__main__": From b5dc9e15b40ba38d2414b7746edd4866585aa75c Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Wed, 2 Sep 2026 09:04:25 +0200 Subject: [PATCH 6/9] fix bug in test-suite execution --- src/freesas/test/utilstest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/freesas/test/utilstest.py b/src/freesas/test/utilstest.py index ed9374f..e0933ea 100644 --- a/src/freesas/test/utilstest.py +++ b/src/freesas/test/utilstest.py @@ -3,6 +3,7 @@ __date__ = "06/02/2026" __copyright__ = "2015-2026, ESRF" +import atexit import logging import os @@ -14,6 +15,16 @@ ) +@atexit.register +def _release_downloader_lock(): + """Drop the filelock held by the downloader while the interpreter is still alive. + + Left to the interpreter shutdown, `BaseFileLock.__del__` runs after the `os` + module globals have been cleared and reports an unraisable TypeError on stderr. + """ + downloader.lock = None + + def get_datafile(name): """Provides the full path of a test file, downloading it from the internet if needed From 8e146a98837db4af3ea30ab4fdc082720abec4a3 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Wed, 2 Sep 2026 11:24:10 +0200 Subject: [PATCH 7/9] ruff-checking --- bootstrap.py | 164 +++++++++++++++++++++++++++++++++------------------ run_tests.py | 7 ++- 2 files changed, 110 insertions(+), 61 deletions(-) diff --git a/bootstrap.py b/bootstrap.py index e6b4208..71b28a8 100755 --- a/bootstrap.py +++ b/bootstrap.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ Bootstrap helps you to test scripts without installing them by patching your PYTHONPATH on the fly @@ -10,20 +9,21 @@ __authors__ = ["Frédéric-Emmanuel Picca", "Jérôme Kieffer"] __contact__ = "jerome.kieffer@esrf.eu" __license__ = "MIT" -__date__ = "12/07/2024" +__date__ = "02/09/2026" -import sys +import argparse +import logging import os import subprocess -import logging -if sys.version_info[:2] < (3, 11): - import tomli -else: +import sys + +try: import tomllib as tomli +except ImportError: + import tomli logging.basicConfig() logger = logging.getLogger("bootstrap") -LIBPATH = "" def get_project_name(root_dir): """Retrieve project name by running python setup.py --name in root_dir. @@ -50,9 +50,10 @@ def build_project(name, root_dir): libdir = "Lib" # extra = ["--buildtype", "plain"] - build = os.path.join(root_dir, "build") + build_dir = f"build_py{sys.version_info[0]}{sys.version_info[1]}" + build = os.path.join(root_dir, build_dir) if not(os.path.isdir(build) and os.path.isdir(os.path.join(build, name))): - p = subprocess.Popen(["meson", "setup", "build"], + p = subprocess.Popen(["meson", "setup", build_dir], shell=False, cwd=root_dir, env=os.environ) p.wait() p = subprocess.Popen(["meson", "configure", "--prefix", "/"] + extra, @@ -77,12 +78,14 @@ def build_project(name, root_dir): home = os.path.join(build, libdir, python_version, "site-packages") home = os.path.abspath(home) - cnt = 0 + tmp = [] while not os.path.isdir(home): - cnt += 1 - home = os.path.split(home)[0] - for _ in range(cnt): - n = os.listdir(home)[0] + home, last = os.path.split(home) + tmp.append(last) + for _ in tmp: + for n in os.listdir(home): + if os.path.isdir(os.path.join(home, n)): + break home = os.path.join(home, n) logger.warning("Building %s to %s", name, home) @@ -98,7 +101,7 @@ def execfile(fullpath, globals=None, locals=None): except UnicodeDecodeError: raise SyntaxError("Not a Python script") code = compile(data, fullpath, 'exec') - exec(code, globals, locals) + exec(code, globals, locals) # noqa: S102 def run_file(filename, argv): @@ -117,14 +120,14 @@ def run_file(filename, argv): # Providing globals() as locals will force to feed the file into # globals() (for examples imports). # Without this any function call from the executed file loses imports - old_argv = sys.argv - sys.argv = full_args - logger.info("Patch the sys.argv: %s", sys.argv) - logger.info("Executing %s.main()", filename) - print("########### EXECFILE ###########") - module_globals = globals().copy() - module_globals['__file__'] = filename try: + old_argv = sys.argv + sys.argv = full_args + logger.info("Patch the sys.argv: %s", sys.argv) + logger.info("Executing %s.main()", filename) + print("########### EXECFILE ###########") + module_globals = globals().copy() + module_globals['__file__'] = filename execfile(filename, module_globals, module_globals) finally: sys.argv = old_argv @@ -155,10 +158,10 @@ def run_entry_point(target_name, entry_point, argv): logger.info("Execute target %s (function %s from module %s) using importlib", target_name, function_name, module_name) full_args = [target_name] full_args.extend(argv) - old_argv = sys.argv - sys.argv = full_args - print("########### IMPORTLIB ###########") try: + old_argv = sys.argv + sys.argv = full_args + print("########### IMPORTLIB ###########") module = importlib.import_module(module_name) if hasattr(module, function_name): func = getattr(module, function_name) @@ -177,7 +180,7 @@ def find_executable(target): - Then search the script from the PATH environment variable. :param str target: Name of the script - :returns: Returns a tuple: kind, name. + :returns: Returns a tuple: (kind, name) or (kind, name, entry_point). """ if os.path.isfile(target): return ("path", os.path.abspath(target)) @@ -192,50 +195,95 @@ def find_executable(target): for script, entry_point in scripts.items(): if script == target: - print(script, entry_point) + #print(script, entry_point) return ("entry_point", target, entry_point) return None, None -PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) -PROJECT_NAME = get_project_name(PROJECT_DIR) -logger.info("Project name: %s", PROJECT_NAME) - -if __name__ == "__main__": - LIBPATH = build_project(PROJECT_NAME, PROJECT_DIR) - if len(sys.argv) < 2: - logger.warning("usage: ./bootstrap.py