diff --git a/pyhdtoolkit/utils/decorators.py b/pyhdtoolkit/utils/decorators.py index df735ba97..b0939eb21 100644 --- a/pyhdtoolkit/utils/decorators.py +++ b/pyhdtoolkit/utils/decorators.py @@ -13,16 +13,19 @@ import inspect import traceback import warnings -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ParamSpec, TypeVar if TYPE_CHECKING: from collections.abc import Callable +P = ParamSpec("P") # for params +R = TypeVar("R") # for returns + # ----- Utility deprecation decorator ----- # -def deprecated(message: str = "") -> Callable: +def deprecated(message: str = "") -> Callable[[Callable[P, R]], Callable[P, R]]: """ Decorator to mark a function as deprecated. It will result in an informative `DeprecationWarning` being issued with the provided @@ -49,22 +52,23 @@ def old_function(): return "I am old!" """ - def decorator_wrapper(func): + def decorator_wrapper(func: Callable[P, R]) -> Callable[P, R]: + last_call_sources: set[str] = set() + @functools.wraps(func) - def function_wrapper(*args, **kwargs): + def function_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: current_call_source = "|".join(traceback.format_stack(inspect.currentframe())) - if current_call_source not in function_wrapper.last_call_source: + + if current_call_source not in last_call_sources: warnings.warn( - f"Function {func.__name__} is now deprecated and will be removed in a future release! {message}", + f"Function {func.__name__} is now deprecated and will be removed in a future release! {message}", # ty:ignore[unresolved-attribute] category=DeprecationWarning, stacklevel=2, ) - function_wrapper.last_call_source.add(current_call_source) + last_call_sources.add(current_call_source) return func(*args, **kwargs) - function_wrapper.last_call_source = set() - return function_wrapper return decorator_wrapper @@ -73,7 +77,9 @@ def function_wrapper(*args, **kwargs): # ----- Utility JIT Compilation decorator ----- # -def maybe_jit(func: Callable, **kwargs) -> Callable: +# We type hint to specify we return a function with the same +# signature as the input function. +def maybe_jit(func: Callable[P, R], **kwargs) -> Callable[P, R]: """ .. versionadded:: 1.7.0 diff --git a/pyproject.toml b/pyproject.toml index 1c02ad576..d9354b85b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ dependencies = [ "numpy >= 2.0", "pandas >= 2.0", "matplotlib >=3.7", - "scipy >= 1.6", + "scipy >= 1.10", "tfs-pandas >= 3.8", "loguru < 1.0", "cpymad >= 1.16", @@ -71,22 +71,22 @@ dependencies = [ [project.optional-dependencies] test = [ "pytest >= 8.0", - "pytest-cov >= 5.0", + "pytest-cov >= 6.0", "pytest-xdist >= 3.0", "numba >= 0.60.0", "flaky >= 3.5", - "pytest-randomly >= 3.3", + "pytest-randomly >= 3.10", "coverage[toml] >= 7.0", "pytest-mpl >= 0.14", ] dev = [ - "ruff >= 0.5", + "ruff >= 0.12", ] docs = [ "joblib >= 1.0", - "Sphinx >= 7.0", - "sphinx-rtd-theme >= 2.0", - "sphinx-issues >= 4.0", + "Sphinx >= 8.0", + "sphinx-rtd-theme >= 3.0", + "sphinx-issues >= 5.0", "sphinx_copybutton < 1.0", "sphinxcontrib-bibtex >= 2.4", "sphinx-design >= 0.6", diff --git a/tests/inputs/utils/correct_user_tasks.pkl b/tests/inputs/utils/correct_user_tasks.pkl index 75ec3b649..ba28833eb 100644 Binary files a/tests/inputs/utils/correct_user_tasks.pkl and b/tests/inputs/utils/correct_user_tasks.pkl differ diff --git a/tests/test_cpymadtools/test_lhc.py b/tests/test_cpymadtools/test_lhc.py index 8cf6c8112..7aaf32e08 100644 --- a/tests/test_cpymadtools/test_lhc.py +++ b/tests/test_cpymadtools/test_lhc.py @@ -487,7 +487,7 @@ def test_rigidity_knob_fails_on_invalid_ir(_non_matched_lhc_madx, caplog): def test_rigidity_knob_fails_on_invalid_side(caplog, _non_matched_lhc_madx): madx = _non_matched_lhc_madx - with pytest.raises(ValueError, match="Invalid value for parameter 'side'."): + with pytest.raises(ValueError, match=r"Invalid value for parameter 'side'."): apply_lhc_rigidity_waist_shift_knob(madx, 1, 1, "invalid") for record in caplog.records: @@ -712,10 +712,10 @@ def test_get_bpms_coupling_rdts(_non_matched_lhc_madx, _reference_twiss_rdts): twiss_with_rdts = get_lhc_bpms_twiss_and_rdts(madx) # We separate the complex components to compare to the reference - twiss_with_rdts["F1001R"] = twiss_with_rdts.F1001.apply(np.real) - twiss_with_rdts["F1001I"] = twiss_with_rdts.F1001.apply(np.imag) - twiss_with_rdts["F1010R"] = twiss_with_rdts.F1010.apply(np.real) - twiss_with_rdts["F1010I"] = twiss_with_rdts.F1010.apply(np.imag) + twiss_with_rdts["F1001R"] = twiss_with_rdts.F1001.apply(np.real) # ty:ignore[unresolved-attribute] + twiss_with_rdts["F1001I"] = twiss_with_rdts.F1001.apply(np.imag) # ty:ignore[unresolved-attribute] + twiss_with_rdts["F1010R"] = twiss_with_rdts.F1010.apply(np.real) # ty:ignore[unresolved-attribute] + twiss_with_rdts["F1010I"] = twiss_with_rdts.F1010.apply(np.imag) # ty:ignore[unresolved-attribute] twiss_with_rdts = twiss_with_rdts.drop(columns=["F1001", "F1010"]).set_index("NAME") # Only care to compare the coupling RDTs columns twiss_with_rdts = twiss_with_rdts.loc[:, ["F1001R", "F1001I", "F1010R", "F1010I"]] @@ -727,8 +727,8 @@ def test_get_bpms_coupling_rdts(_non_matched_lhc_madx, _reference_twiss_rdts): def test_k_modulation(_non_matched_lhc_madx, _reference_kmodulation): madx = _non_matched_lhc_madx results = do_kmodulation(madx) - assert all(var == 0 for var in results.ERRTUNEX) - assert all(var == 0 for var in results.ERRTUNEY) + assert np.all(results.ERRTUNEX.to_numpy() == 0) # ty:ignore[unresolved-attribute] + assert np.all(results.ERRTUNEY.to_numpy() == 0) # ty:ignore[unresolved-attribute] reference = tfs.read(_reference_kmodulation) assert_frame_equal(results.convert_dtypes(), reference.convert_dtypes()) # avoid dtype comparison error on 0 cols @@ -841,7 +841,7 @@ def test_lhc_run3_setup_context_manager_raises_on_wrong_b4_conditions(): @pytest.mark.skipif(not (TESTS_DIR.parent / "acc-models-lhc").is_dir(), reason="acc-models-lhc not found") def test_lhc_run3_setup_context_manager_raises_on_wrong_run_value(): with pytest.raises( # noqa: SIM117 - NotImplementedError, match="This setup is only possible for Run 2 and Run 3 configurations." + NotImplementedError, match=r"This setup is only possible for Run 2 and Run 3 configurations." ): # using b4 with beam1 setup crashes with LHCSetup(run=1, opticsfile="R2022a_A30cmC30cmA10mL200cm.madx") as madx: # noqa: F841 pass diff --git a/tests/test_plotting/test_aperture.py b/tests/test_plotting/test_aperture.py index 2af579f0d..80ecd082e 100644 --- a/tests/test_plotting/test_aperture.py +++ b/tests/test_plotting/test_aperture.py @@ -87,5 +87,5 @@ def test_plot_physical_apertures_ir5_collision_vertical(_collision_aperture_tole def test_plot_physical_apertures_raises_on_wrong_plane(): madx = Madx(stdout=False) - with pytest.raises(ValueError, match="Invalid 'plane' argument."): + with pytest.raises(ValueError, match=r"Invalid 'plane' argument."): plot_physical_apertures(madx, plane="invalid") diff --git a/tests/test_plotting/test_envelope.py b/tests/test_plotting/test_envelope.py index 5cbafc928..f75f1908b 100644 --- a/tests/test_plotting/test_envelope.py +++ b/tests/test_plotting/test_envelope.py @@ -18,7 +18,7 @@ def test_plot_enveloppe_raises_on_wrong_plane(): madx = Madx(stdout=False) - with pytest.raises(ValueError, match="Invalid 'plane' argument."): + with pytest.raises(ValueError, match=r"Invalid 'plane' argument."): plot_beam_envelope(madx, "lhcb1", plane="invalid") diff --git a/tests/test_plotting/test_helpers.py b/tests/test_plotting/test_helpers.py index 37f9dc8d9..08966e3c0 100644 --- a/tests/test_plotting/test_helpers.py +++ b/tests/test_plotting/test_helpers.py @@ -40,5 +40,5 @@ def test_confidence_ellipse_fails_on_mismatched_dimensions(): def test_default_sbs_coupling_label_raises_on_wrong_component(): - with pytest.raises(ValueError, match="Invalid component for coupling RDT."): + with pytest.raises(ValueError, match=r"Invalid component for coupling RDT."): _determine_default_sbs_coupling_ylabel(rdt="f1001", component="NONEXISTANT") diff --git a/tests/test_plotting/test_phasespace.py b/tests/test_plotting/test_phasespace.py index b4b1db20b..b9d146365 100644 --- a/tests/test_plotting/test_phasespace.py +++ b/tests/test_plotting/test_phasespace.py @@ -87,7 +87,7 @@ def test_plot_courant_snyder_phase_space_wrong_plane_input(): match_cas3(madx) x_coords_stable, px_coords_stable = np.array([]), np.array([]) # no need for tracking - with pytest.raises(ValueError, match="Invalid 'plane' argument."): + with pytest.raises(ValueError, match=r"Invalid 'plane' argument."): plot_courant_snyder_phase_space(madx, x_coords_stable, px_coords_stable, plane="invalid_plane") @@ -97,7 +97,7 @@ def test_plot_courant_snyder_phase_space_colored_wrong_plane_input(): madx.input(BASE_LATTICE) match_cas3(madx) x_coords_stable, px_coords_stable = np.array([]), np.array([]) # no need for tracking - with pytest.raises(ValueError, match="Invalid 'plane' argument."): + with pytest.raises(ValueError, match=r"Invalid 'plane' argument."): plot_courant_snyder_phase_space_colored(madx, x_coords_stable, px_coords_stable, plane="invalid_plane") diff --git a/tests/test_plotting/test_plotting_utils.py b/tests/test_plotting/test_plotting_utils.py index f6924efaf..302122e1c 100644 --- a/tests/test_plotting/test_plotting_utils.py +++ b/tests/test_plotting/test_plotting_utils.py @@ -54,7 +54,7 @@ def test_coupling_ylabel(f1001, f1010, abs_, real, imag): @pytest.mark.parametrize("rdt", ["invalid", "F1111", "nope"]) def test_coupling_ylabel_raises_on_invalid_rdt(rdt): - with pytest.raises(ValueError, match="Invalid RDT for coupling plot."): + with pytest.raises(ValueError, match=r"Invalid RDT for coupling plot."): _determine_default_sbs_coupling_ylabel(rdt, "abs") @@ -69,7 +69,7 @@ def test_phase_ylabel(plane): @pytest.mark.parametrize("plane", ["a", "Fb1", "nope", "not a plane"]) def test_phase_ylabel_raises_on_invalid_plane(plane): - with pytest.raises(ValueError, match="Invalid plane for phase plot."): + with pytest.raises(ValueError, match=r"Invalid plane for phase plot."): _determine_default_sbs_phase_ylabel(plane) diff --git a/tests/test_plotting/test_sbs_phase.py b/tests/test_plotting/test_sbs_phase.py index c5ad8e1e3..1a3c13e26 100644 --- a/tests/test_plotting/test_sbs_phase.py +++ b/tests/test_plotting/test_sbs_phase.py @@ -42,7 +42,7 @@ def test_plot_both_beams(sbs_phasex, sbs_phasey, sbs_model_b2): @pytest.mark.parametrize("wrongplane", ["not", "accepted", "incorrect", ""]) def test_plot_phase_segment_raises_on_wrong_plane(wrongplane, sbs_phasex, sbs_model_b2): - with pytest.raises(ValueError, match="Invalid 'plane' argument."): + with pytest.raises(ValueError, match=r"Invalid 'plane' argument."): plot_phase_segment(segment_df=sbs_phasex, model_df=sbs_model_b2, plane=wrongplane) diff --git a/tests/test_plotting/test_tunediagram.py b/tests/test_plotting/test_tunediagram.py index 806b7d4c2..fe202d5ee 100644 --- a/tests/test_plotting/test_tunediagram.py +++ b/tests/test_plotting/test_tunediagram.py @@ -38,7 +38,7 @@ def test_plot_tune_diagram_colored_by_resonance_order(): @pytest.mark.parametrize("max_order", [2, 3, 4, 5]) @pytest.mark.parametrize("differentiate", [False, True]) def test_plot_tune_diagram_arguments(figure_title, legend_title, max_order, differentiate): - figure, ax = plt.subplots(figsize=(10, 10)) + _figure, ax = plt.subplots(figsize=(10, 10)) plot_tune_diagram( title=figure_title, legend_title=legend_title, diff --git a/tests/test_utils.py b/tests/test_utils.py index 5f6145adb..0207c0bae 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,6 +5,7 @@ import subprocess import sys import time +from collections.abc import Iterator import numpy as np import pandas as pd @@ -45,7 +46,7 @@ def test_check_pid(self): assert CommandLine.check_pid_exists(0) is True assert CommandLine.check_pid_exists(int(1e6)) is False # default max PID is 32768 on linux, 99999 on macOS with pytest.raises(TypeError): - CommandLine.check_pid_exists("not_an_integer") + CommandLine.check_pid_exists("not_an_integer") # ty:ignore[invalid-argument-type] def test_run_cmd(self): assert isinstance(CommandLine.run("echo hello"), tuple) @@ -70,7 +71,10 @@ def test_terminate_nonexistent_pid(self, pid): @pytest.mark.parametrize("sleep_time", list(range(10, 60))) # each one will spawn a different process def test_terminate_pid(self, sleep_time): sacrificed_process = subprocess.Popen(f"sleep {sleep_time}", shell=True) - assert CommandLine.terminate(sacrificed_process.pid) is True + try: + assert CommandLine.terminate(sacrificed_process.pid) is True + finally: # the process would hang, we make sure to cleanup + sacrificed_process.wait() class TestHTCMonitor: @@ -101,11 +105,11 @@ def test_cluster_table_creation(self, _condor_q_output): assert isinstance(cluster_table, Table) def test_tasks_table_creation(self, _condor_q_output, _taskless_condor_q_output): - user_tasks, cluster_info = read_condor_q(_condor_q_output) + user_tasks, _ = read_condor_q(_condor_q_output) tasks_table = _make_tasks_table(user_tasks) assert isinstance(tasks_table, Table) - user_tasks, cluster_info = read_condor_q(_taskless_condor_q_output) + user_tasks, _ = read_condor_q(_taskless_condor_q_output) tasks_table = _make_tasks_table(user_tasks) assert isinstance(tasks_table, Table) @@ -133,7 +137,7 @@ def test_query_betastar_from_opticsfile(self): def test_query_betastar_from_opticsfile_raises_on_invalid_symmetry_if_required(self): with pytest.raises( - AssertionError, match="The betastar values for IP1 and IP5 are not the same in both planes." + AssertionError, match=r"The betastar values for IP1 and IP5 are not the same in both planes." ): _misc.get_betastar_from_opticsfile(INPUTS_DIR / "madx" / "opticsfile.asymmetric", check_symmetry=True) @@ -239,24 +243,24 @@ def _taskless_condor_q_output() -> str: @pytest.fixture -def _correct_user_tasks() -> list[HTCTaskSummary]: +def _correct_user_tasks() -> Iterator[list[HTCTaskSummary]]: pickle_file_path = INPUTS_DIR / "utils" / "correct_user_tasks.pkl" with pickle_file_path.open("rb") as file: - return pickle.load(file) + yield pickle.load(file) @pytest.fixture -def _correct_cluster_summary() -> ClusterSummary: +def _correct_cluster_summary() -> Iterator[ClusterSummary]: pickle_file_path = INPUTS_DIR / "utils" / "correct_cluster_summary.pkl" with pickle_file_path.open("rb") as file: - return pickle.load(file) + yield pickle.load(file) @pytest.fixture def _complex_columns_df() -> pd.DataFrame: rng = np.random.default_rng() array = rng.random(size=(50, 5)) + 1j * rng.random(size=(50, 5)) - return pd.DataFrame(data=array, columns=["A", "B", "C", "D", "E"]) + return pd.DataFrame(data=array, columns=["A", "B", "C", "D", "E"]) # ty:ignore[invalid-argument-type] @pytest.fixture diff --git a/uv.lock b/uv.lock index de5b718a2..9d578d768 100644 --- a/uv.lock +++ b/uv.lock @@ -1318,20 +1318,20 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'all'", specifier = ">=8.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0" }, - { name = "pytest-cov", marker = "extra == 'all'", specifier = ">=5.0" }, - { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0" }, + { name = "pytest-cov", marker = "extra == 'all'", specifier = ">=6.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=6.0" }, { name = "pytest-mpl", marker = "extra == 'all'", specifier = ">=0.14" }, { name = "pytest-mpl", marker = "extra == 'test'", specifier = ">=0.14" }, - { name = "pytest-randomly", marker = "extra == 'all'", specifier = ">=3.3" }, - { name = "pytest-randomly", marker = "extra == 'test'", specifier = ">=3.3" }, + { name = "pytest-randomly", marker = "extra == 'all'", specifier = ">=3.10" }, + { name = "pytest-randomly", marker = "extra == 'test'", specifier = ">=3.10" }, { name = "pytest-xdist", marker = "extra == 'all'", specifier = ">=3.0" }, { name = "pytest-xdist", marker = "extra == 'test'", specifier = ">=3.0" }, { name = "rich", specifier = ">=13.0" }, - { name = "ruff", marker = "extra == 'all'", specifier = ">=0.5" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5" }, - { name = "scipy", specifier = ">=1.6" }, - { name = "sphinx", marker = "extra == 'all'", specifier = ">=7.0" }, - { name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.0" }, + { name = "ruff", marker = "extra == 'all'", specifier = ">=0.12" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12" }, + { name = "scipy", specifier = ">=1.10" }, + { name = "sphinx", marker = "extra == 'all'", specifier = ">=8.0" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=8.0" }, { name = "sphinx-codeautolink", marker = "extra == 'all'", specifier = ">=0.14" }, { name = "sphinx-codeautolink", marker = "extra == 'docs'", specifier = ">=0.14" }, { name = "sphinx-copybutton", marker = "extra == 'all'", specifier = "<1.0" }, @@ -1340,12 +1340,12 @@ requires-dist = [ { name = "sphinx-design", marker = "extra == 'docs'", specifier = ">=0.6" }, { name = "sphinx-gallery", marker = "extra == 'all'", specifier = "<1.0" }, { name = "sphinx-gallery", marker = "extra == 'docs'", specifier = "<1.0" }, - { name = "sphinx-issues", marker = "extra == 'all'", specifier = ">=4.0" }, - { name = "sphinx-issues", marker = "extra == 'docs'", specifier = ">=4.0" }, + { name = "sphinx-issues", marker = "extra == 'all'", specifier = ">=5.0" }, + { name = "sphinx-issues", marker = "extra == 'docs'", specifier = ">=5.0" }, { name = "sphinx-prompt", marker = "extra == 'all'", specifier = ">=1.5" }, { name = "sphinx-prompt", marker = "extra == 'docs'", specifier = ">=1.5" }, - { name = "sphinx-rtd-theme", marker = "extra == 'all'", specifier = ">=2.0" }, - { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=2.0" }, + { name = "sphinx-rtd-theme", marker = "extra == 'all'", specifier = ">=3.0" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=3.0" }, { name = "sphinxcontrib-bibtex", marker = "extra == 'all'", specifier = ">=2.4" }, { name = "sphinxcontrib-bibtex", marker = "extra == 'docs'", specifier = ">=2.4" }, { name = "tfs-pandas", specifier = ">=3.8" },