From 3bee67a5d63837638fc14df1184c6d6e7b246d99 Mon Sep 17 00:00:00 2001 From: dodu94 Date: Tue, 27 Oct 2026 12:25:59 +0100 Subject: [PATCH 1/8] update dependencies --- .readthedocs.yaml | 5 +++- AGENTS.md | 2 +- docs/source/requirements.txt | 20 ---------------- docs/source/usage/installation.rst | 19 +++++++++++---- pyproject.toml | 37 +++++++++++++++--------------- 5 files changed, 39 insertions(+), 44 deletions(-) delete mode 100644 docs/source/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 162e454d..bc412e42 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -27,4 +27,7 @@ sphinx: # Optionally declare the Python requirements required to build your docs python: install: - - requirements: docs/source/requirements.txt + - method: pip + path: . + extra_requirements: + - docs diff --git a/AGENTS.md b/AGENTS.md index f9e5d404..6b22c84b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ JADE is a Python V&V (Verification & Validation) framework for nuclear data libr ## Setup ```bash -pip install -e .[dev,ui] # dev tools: pytest, ruff, sphinx; ui: ttkthemes +pip install -e .[dev,ui,docs] # dev: pytest, ruff; ui: ttkthemes; docs: sphinx ``` ## Running Tests diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt deleted file mode 100644 index e1710df6..00000000 --- a/docs/source/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -# JADE reqs -pandas -numjuggler -tqdm -xlsxwriter -openpyxl -matplotlib -scipy -python-docx -aspose-words -requests -f4enix >= 0.12.0 -seaborn -# DOC reqs -sphinx -esbonio -myst-parser -pydata-sphinx-theme -pyyaml -pygments \ No newline at end of file diff --git a/docs/source/usage/installation.rst b/docs/source/usage/installation.rst index d0dc56dc..489b584a 100644 --- a/docs/source/usage/installation.rst +++ b/docs/source/usage/installation.rst @@ -23,7 +23,7 @@ Or if you are using Anaconda: | ``conda activate jade`` .. warning:: - JADE officially supports only Python 3.10, 3.11 and 3.12. + JADE officially supports only Python 3.11, 3.12 and 3.13. It may work with other versions, but they are untested. Install the JADE package @@ -74,10 +74,21 @@ You can either dowload the zip file or clone it using git with: Moving into the downloaded folder, the user can install the ``jade`` python package through a local pip install. -It is recommeded to use the '-e' option when installing (editable mode) -and you should also install the additional 'dev' dependencies. +It is recommended to use the '-e' option when installing (editable mode), +together with the optional dependency groups needed for development: - | ``pip install -e .[dev]`` + | ``pip install -e .[dev,ui,docs]`` + +The available optional dependency groups are: + +- ``dev``: testing and formatting tools (``pytest``, ``pytest-cov``, ``pytest-xvfb``, ``ruff``). +- ``ui``: GUI dependencies (``ttkthemes``). Skip it on systems without Tkinter. +- ``docs``: tools needed to build this documentation locally (``sphinx`` and its extensions). + +All dependencies are declared in ``pyproject.toml``, which is the single source of truth +(the documentation build on Read the Docs also installs from it). Most dependencies are capped +at their current major version (minor version for ``0.x`` packages); when bumping a cap, +make sure the test suite still passes. If your dev system allows it, install also openmc in the same python environment. diff --git a/pyproject.toml b/pyproject.toml index 98f5f2e4..8f8f79ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,31 +18,32 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "xlsxwriter", - "matplotlib", - "aspose-words", - "requests", - "f4enix >= 1.0.0", - "pyyaml", - "seaborn", - "python-gitlab" + "xlsxwriter < 4", + "matplotlib < 4", + "requests < 3", + "f4enix >= 1.2.1, < 2", + "pyyaml < 7", + "seaborn < 0.14", + "python-gitlab < 9" ] [project.optional-dependencies] dev = [ - "pytest", - "pytest-cov", - "pytest-xvfb", # needed for headless display + "pytest < 10", + "pytest-cov < 8", + "pytest-xvfb < 4", # needed for headless display "ruff", # best formatter that should be used by everybody - "sphinx", - "myst-parser", - "sphinx-book-theme", - "sphinx-copybutton", - "sphinx-togglebutton", - "sphinx-design" +] +docs = [ + "sphinx < 10", + "myst-parser < 6", + "sphinx-book-theme < 2", + "sphinx-copybutton < 0.6", + "sphinx-togglebutton < 0.5", + "sphinx-design < 0.8" ] ui = [ - "ttkthemes" + "ttkthemes < 4" ] [project.urls] From e396a0868392b3ef0bbc7f05959178fe8a1a9c0e Mon Sep 17 00:00:00 2001 From: dodu94 Date: Tue, 27 Oct 2026 12:34:02 +0100 Subject: [PATCH 2/8] update python supported versions --- .github/workflows/pytest.yml | 18 ++++++++---------- AGENTS.md | 23 ++++++++++++----------- docs/source/usage/installation.rst | 2 +- pyproject.toml | 2 +- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 04e10b71..21c8fa9e 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -2,7 +2,7 @@ name: pytest # Controls when the workflow will run on: [push, pull_request, workflow_dispatch] - # Triggers the workflow on push or pull request events but only for the master branch + # Triggers the workflow on push or pull request events for all branches # workflow_dispatch allows you to run this workflow manually from the Actions tab jobs: @@ -10,17 +10,15 @@ jobs: # The type of runner and python versions that the job will run on strategy: matrix: - os: [windows-latest, ubuntu-20.04, ubuntu-latest] - python-version: ["3.11", '3.12', '3.13'] + os: [windows-latest, ubuntu-latest] + python-version: ['3.11', '3.12', '3.13', '3.14'] full-test: - - ${{ contains(github.ref, 'master') || contains(github.ref, 'main') || startsWith(github.ref, 'refs/heads/release') }} + - ${{ github.ref == 'refs/heads/developing' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release') }} exclude: - full-test: false - python-version: ["3.10"] + python-version: '3.11' - full-test: false - python-version: ["3.11"] - - full-test: false - os: "ubuntu-20.04" + python-version: '3.12' fail-fast: false runs-on: ${{ matrix.os }} @@ -108,8 +106,8 @@ jobs: # Upload coverage to Codecov. Do it only for one case of the matrix. - name: Upload coverage to Codecov - # do it only once for python 3.12 - if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest' + # do it only once for python 3.13 (runs on every branch) + if: matrix.python-version == '3.13' && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v4 with: verbose: true diff --git a/AGENTS.md b/AGENTS.md index 6b22c84b..aced5992 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ JADE is a Python V&V (Verification & Validation) framework for nuclear data libr - **Documentation**: [ReadTheDocs](https://jade-a-nuclear-data-libraries-vv-tool.readthedocs.io/en/latest/) - **PyPI package**: `jadevv` (imports as `jade`) -- **Python**: ≥ 3.10 +- **Python**: ≥ 3.11 --- @@ -59,8 +59,8 @@ Every transport-code-extensible concept uses this pattern: ```python # helper/constants.py -class CODE(str, Enum): - mcnp = "mcnp"; openmc = "openmc"; serpent = "serpent"; d1s = "d1s"; exp = "exp" +class CODE(Enum): + MCNP = "mcnp"; OPENMC = "openmc"; SERPENT = "serpent"; D1S = "d1s"; ACTINV = "actinv"; EXPERIMENT = "exp" # e.g. run/benchmark.py class SingleRun(ABC): ... @@ -70,7 +70,7 @@ class SingleRunFactory: def create(code: CODE, ...) -> SingleRun: ... ``` -The same pattern applies to `Library`/`LibraryFactory`, `Input`/`InputFactory`, and `AbstractSimOutput`. +The same pattern applies to `Library`/`LibraryFactory`. `Input` subclasses are selected by `CODE` inside `BenchmarkRun` (`run/benchmark.py`), and `AbstractSimOutput` subclasses inside `RawProcessor.__init__()` (`post/raw_processor.py`). ### Config construction: always via named classmethods @@ -96,7 +96,7 @@ Expensive state (e.g. `GlobalStatus.simulations`) uses `@property` with an inter ### `from __future__ import annotations` -Every module starts with this import. +New modules should start with this import (most existing modules already do). --- @@ -114,8 +114,8 @@ See [dev guide](docs/source/dev/add_transport_code.rst) for full details. The fo 1. `src/jade/helper/constants.py` — add tag to `CODE` enum 2. `src/jade/config/run_config.py` — subclass `Library`, register in `LibraryFactory` -3. `src/jade/run/benchmark.py` + `src/jade/run/input.py` — subclass `SingleRun` (implement `_build_command()`, `_get_lib_data_command()`) and `Input` (implement `set_nps()`, `translate()`, `_write()`); register in `SingleRunFactory` -4. `src/jade/post/sim_output.py` — subclass `AbstractSimOutput`; wire into `RawProcessor.__init__()` +3. `src/jade/run/benchmark.py` + `src/jade/run/input.py` — subclass `SingleRun` (implement `_build_command()`, `_get_lib_data_command()`) and `Input` (implement `set_nps()`, `translate()`, `_write()`); register in `SingleRunFactory` and in the input selection of `BenchmarkRun` +4. `src/jade/post/sim_output.py` — subclass `AbstractSimOutput`; wire into `RawProcessor.__init__()` in `src/jade/post/raw_processor.py` Add tests mirroring the new code's path under `tests/`. @@ -135,7 +135,7 @@ Input files are hosted externally (not in this repo): IAEA GitHub for open bench ## Branching & Versioning -- Permanent branches: `master` (stable), `developing` (integration) +- Permanent branches: `developing` (default, integration), `master` (stable) - Branch types: `Feature/*` and `Benchmark/*` off `developing`; `Release/*` for stabilization; `Hotfix/*` off `master` - **Semantic versioning**: Major = non-backwards-compatible change (new transport code, class restructure, folder layout). Minor = new features/benchmarks. Patch = bug fixes. - Every bug fix must include a regression test. @@ -145,7 +145,8 @@ Input files are hosted externally (not in this repo): IAEA GitHub for open bench ## CI Matrix -- **OS**: `ubuntu-latest`, `ubuntu-20.04`, `windows-latest` -- **Python**: 3.11, 3.12, 3.13 -- Full matrix only on `master`/`release*`; feature branches run latest Python on Ubuntu only. +- **OS**: `ubuntu-latest`, `windows-latest` +- **Python**: 3.11, 3.12, 3.13, 3.14 +- Full matrix only on pushes to `developing`/`master`/`release*`; other branches and PRs run only the two latest Python versions (both OSes). +- Coverage is uploaded to Codecov from the Ubuntu / Python 3.13 job. - OpenMC is installed on Linux only; Windows CI skips OpenMC tests. diff --git a/docs/source/usage/installation.rst b/docs/source/usage/installation.rst index 489b584a..ffd206a3 100644 --- a/docs/source/usage/installation.rst +++ b/docs/source/usage/installation.rst @@ -23,7 +23,7 @@ Or if you are using Anaconda: | ``conda activate jade`` .. warning:: - JADE officially supports only Python 3.11, 3.12 and 3.13. + JADE officially supports only Python 3.11, 3.12, 3.13 and 3.14. It may work with other versions, but they are untested. Install the JADE package diff --git a/pyproject.toml b/pyproject.toml index 8f8f79ea..bb64c353 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ ] description = "JADE, a V&V tool for nuclear data libraries and transport codes" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)", From cd78ac1180613aa5bb4fdb7d613ac146da9bb020 Mon Sep 17 00:00:00 2001 From: dodu94 Date: Tue, 27 Oct 2026 14:23:25 +0100 Subject: [PATCH 3/8] fix deprecations and new f4enix behaviours --- src/jade/app/fetch.py | 13 +++++++++- src/jade/post/manipulate_tally.py | 2 +- src/jade/run/input.py | 23 ++++++++++------- tests/run/test_input.py | 43 ++++++++++++++++++++++++++++++- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/jade/app/fetch.py b/src/jade/app/fetch.py index 44b5a6e2..24284f71 100644 --- a/src/jade/app/fetch.py +++ b/src/jade/app/fetch.py @@ -16,6 +16,7 @@ IAEA_URL = r"https://github.com/IAEA-NDS/open-benchmarks/archive/main.zip" RAW_DATA_GITHUB_URL = r"https://github.com/JADE-V-V/JADE-RAW-RESULTS/archive/main.zip" +GITLAB_TIMEOUT = 30 # seconds def _fetch_from_git( @@ -90,13 +91,22 @@ def fetch_from_gitlab( extracted_folder: Pathlike | bool path to the extracted folder. False if the download was not successful. """ - gl = gitlab.Gitlab(url=url, private_token=authorization_token, ssl_verify=False) + gl = gitlab.Gitlab( + url=url, + private_token=authorization_token, + ssl_verify=False, + timeout=GITLAB_TIMEOUT, + ) try: gl.auth() except gitlab.exceptions.GitlabAuthenticationError as e: logger.error("Gitlab authentication failed") logger.error(e) return False + except requests.exceptions.RequestException as e: + logger.error(f"Could not connect to {url}") + logger.error(e) + return False # select the correct project found = False @@ -125,6 +135,7 @@ def _install_data(fetch_folder: str | os.PathLike, install_folder: str | os.Path os.path.join(install_folder, item), ) + def _install_standard_folder_structure( extracted_folder: str | os.PathLike, inputs_root: PathLike, diff --git a/src/jade/post/manipulate_tally.py b/src/jade/post/manipulate_tally.py index 3a3a5f9e..8b405525 100644 --- a/src/jade/post/manipulate_tally.py +++ b/src/jade/post/manipulate_tally.py @@ -260,7 +260,7 @@ def groupby(tally: pd.DataFrame, by: str, action: str) -> pd.DataFrame: # a series has been created but we want a df df = df.to_frame().T else: - df.reset_index(inplace=True) + df = df.reset_index() return df diff --git a/src/jade/run/input.py b/src/jade/run/input.py index 0aefafe5..2b139f0a 100644 --- a/src/jade/run/input.py +++ b/src/jade/run/input.py @@ -192,7 +192,9 @@ def _get_material(self, zaid: str | Material) -> Material: if isinstance(zaid, Material): material = deepcopy(zaid) # just to be sure not to mess with something truename = material.name - material.header = f"{material.header}C\nC True name:{truename}" + # f4enix strips the trailing newline from headers + header = f"{material.header}\n" if material.header else "" + material.header = f"{header}C\nC True name:{truename}" material.name = "M1" # override the input name self._name = f"{self.name}_{truename}" @@ -283,7 +285,9 @@ def _get_material(self, zaid: str | Material) -> Material: if isinstance(zaid, Material): material = deepcopy(zaid) # just to be sure not to mess with something truename = material.name - material.header = f"{material.header}C\nC True name:{truename}" + # f4enix strips the trailing newline from headers + header = f"{material.header}\n" if material.header else "" + material.header = f"{header}C\nC True name:{truename}" material.name = "M1" # override the input name self._name = f"{self.name}_{truename}" @@ -445,12 +449,13 @@ def __init__( if isinstance(zaid, Material): if self.lib.suffix is None: raise ConfigError("suffix must be provided for D1S libraries") - self.inp.reac_file = ReactionFile( - self.inp.get_potential_paths(self.lm, lib.suffix) - ) - daughters = [] - for reac in self.inp.reac_file.reactions: - daughters.append(reac.daughter) + # keep only reactions whose daughters are in the irradiation file, + # and only the irradiations of daughters produced by those reactions + self.inp.get_reaction_file(self.lm, lib.suffix, set_as_attribute=True) + daughters = [ + reac.daughter.write_to_int_string() + for reac in self.inp.reac_file.reactions + ] self.inp.irrad_file.select_daughters_irradiation_file(daughters) self._name = f"{self.name}_All" else: @@ -461,7 +466,7 @@ def __init__( reaction = Reaction( Nuclide.from_int_string(f"{zaid}.{lib.suffix}"), MT, - Nuclide.from_int_string(f"{daughter}.{lib.suffix}"), + Nuclide.from_int_string(daughter), ) reacfile = ReactionFile([reaction]) self.inp.reac_file = reacfile diff --git a/tests/run/test_input.py b/tests/run/test_input.py index 4366ddfa..19a1708c 100644 --- a/tests/run/test_input.py +++ b/tests/run/test_input.py @@ -6,8 +6,9 @@ import f4enix.input.MCNPinput as ipt import pytest +from f4enix.input.d1suned import IrradiationFile, ReactionFile from f4enix.input.libmanager import LibManager -from f4enix.input.materials import Zaid +from f4enix.input.materials import Material, Zaid import jade.resources.default_cfg.benchmarks as bench import tests.dummy_structure.benchmark_templates as res @@ -117,6 +118,24 @@ def test_input_generation(self, libMCNP, tmpdir): assert nps_line[:6] == "NPS 10" assert read_inp.mat_section["M1"].zaids[0].library == "31c" + def test_material_header(self, libMCNP, tmpdir): + # header without trailing newline (as stored by f4enix) must not be + # glued to the extra comment lines, producing an invalid "CC" line + template_folder = TEMPLATE_ROOT.joinpath("Sphere/Sphere/mcnp") + zaid = Zaid.from_string("1001.31c 1.0") + material = Material("M101", [zaid], header="C my material\nC") + inp = InputMCNPSphere(template_folder, libMCNP, material, "1.0") + inp.write(tmpdir) + + read_inp = ipt.Input.from_input(Path(tmpdir).joinpath("Sphere_M101.i")) + header = read_inp.mat_section["M1"].header + assert header.splitlines() == [ + "C my material", + "C", + "C", + "C True name:M101", + ] + @pytest.mark.skipif(not OMC_AVAIL, reason="OpenMC not available") class TestInputOpenMCSphere: @@ -148,3 +167,25 @@ def test_input_generation(self, tmpdir, libD1S): assert os.path.exists(Path(tmpdir).joinpath("SphereSDDR_11023_Na-23_16.i")) assert os.path.getsize(Path(tmpdir).joinpath("irrad")) > 0 assert os.path.getsize(Path(tmpdir).joinpath("react")) > 0 + + def test_input_generation_material(self, tmpdir, libD1S): + # the irradiation file must keep exactly the daughters produced by the + # reactions (it used to be silently emptied) + template_folder = TEMPLATE_ROOT.joinpath("SphereSDDR/SphereSDDR/d1s") + irrad_file = CONFIG_BENCH.joinpath("SphereSDDR/irrad_99c.txt") + materials = ipt.Input.from_input( + TEMPLATE_ROOT.joinpath("TypicalMaterials") + ).mat_section + inp = InputD1SSphere( + template_folder, libD1S, materials["M101"], "1.0", irrad_file=irrad_file + ) + inp.set_nps(10) + inp.translate() + inp.write(tmpdir) + + irrad = IrradiationFile.from_text(Path(tmpdir).joinpath("irrad")) + react = ReactionFile.from_text(Path(tmpdir).joinpath("react")) + irrad_daughters = {d.write_to_int_string() for d in irrad.get_daughters()} + react_daughters = {r.daughter.write_to_int_string() for r in react.reactions} + assert len(irrad_daughters) > 0 + assert irrad_daughters == react_daughters From cf58175e03d0767596fa22ddf9412d32ce44f939 Mon Sep 17 00:00:00 2001 From: dodu94 Date: Tue, 27 Oct 2026 14:38:14 +0100 Subject: [PATCH 4/8] solve future deprecation warnings --- src/jade/post/excel_routines.py | 8 ++--- src/jade/post/plotter.py | 10 +++--- src/jade/post/sim_output.py | 14 ++++----- tests/post/test_plotter.py | 56 ++++++++++++++++++++++++++++++++- 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/jade/post/excel_routines.py b/src/jade/post/excel_routines.py index 378870fd..b4fa915c 100644 --- a/src/jade/post/excel_routines.py +++ b/src/jade/post/excel_routines.py @@ -180,7 +180,7 @@ def _rename_columns(df: pd.DataFrame, names: dict[str, str]) -> None: col_names = {k: v for k, v in names.items() if k in df.columns.names} df.columns = df.columns.rename(col_names) else: - df.rename(columns=names, inplace=True) + df.columns = df.rename(columns=names).columns @staticmethod def _select_common_index_data( @@ -223,7 +223,7 @@ def _get_sheet(self) -> list[pd.DataFrame]: ) # this is needed to avoid NaN in the multiindex which would cause incorrect dump value_df.columns = pd.MultiIndex.from_frame( - value_df.columns.to_frame().fillna("") + value_df.columns.to_frame().astype(object).fillna("") ) if self.cfg.add_error: @@ -233,7 +233,7 @@ def _get_sheet(self) -> list[pd.DataFrame]: # this is needed to avoid NaN in the multiindex which would cause incorrect # dump ref_err.columns = pd.MultiIndex.from_frame( - ref_err.columns.to_frame().fillna("") + ref_err.columns.to_frame().astype(object).fillna("") ) target_err = self.target_df.pivot( @@ -242,7 +242,7 @@ def _get_sheet(self) -> list[pd.DataFrame]: # this is needed to avoid NaN in the multiindex which would cause incorrect # dump target_err.columns = pd.MultiIndex.from_frame( - target_err.columns.to_frame().fillna("") + target_err.columns.to_frame().astype(object).fillna("") ) return [value_df, ref_err, target_err] diff --git a/src/jade/post/plotter.py b/src/jade/post/plotter.py index 2358c8b9..25ffd726 100644 --- a/src/jade/post/plotter.py +++ b/src/jade/post/plotter.py @@ -753,9 +753,9 @@ def _get_figure(self) -> tuple[Figure, Axes]: if isinstance(axes, Axes): axes = [axes] + data = [(codelib, df.set_index("User")) for codelib, df in self.data] indices = [] - for idx, (codelib, df) in enumerate(self.data): - df.set_index("User", inplace=True) + for _, df in data: indices.extend(df.index.to_list()) # build a common index of isotopes so that the colors are the same for all @@ -766,7 +766,7 @@ def _get_figure(self) -> tuple[Figure, Axes]: for idx, isotope in enumerate(isotopes): if int(isotope) == 0: continue - for i, (codelib, df) in enumerate(self.data): + for i, (_, df) in enumerate(data): if i == 0: _, label = LM.get_zaidname(str(abs(isotope))) else: @@ -788,7 +788,7 @@ def _get_figure(self) -> tuple[Figure, Axes]: linewidth=0.5, ) - for i, ax in enumerate(axes): + for ax, (codelib, _) in zip(axes, data): # --- Plot details --- # ax details ax.set_title(codelib) @@ -1177,6 +1177,8 @@ def _shorten_x_name(ax: Axes, shorten_x_name: int) -> None: for label in ax.get_xticklabels(): split = label.get_text().split("_") new_labels.append(" ".join(split[-shorten_x_name:])) + # fix the current ticks so that the new labels stay attached to them + ax.set_xticks(ax.get_xticks()) ax.set_xticklabels(new_labels) diff --git a/src/jade/post/sim_output.py b/src/jade/post/sim_output.py index 1afa64e0..e5bb0d70 100644 --- a/src/jade/post/sim_output.py +++ b/src/jade/post/sim_output.py @@ -130,7 +130,7 @@ def __init__( tallydata = mctal.tallydata total_bin = mctal.totalbin for dictionary in [tallydata, total_bin]: - for _, df in dictionary.items(): + for key, df in dictionary.items(): if df is not None: if ( "Cells" in df.columns @@ -156,9 +156,8 @@ def __init__( if len(usr_bins) <= 2 and "total" in usr_bins: # then the column does not add any additional info, to drop del df["User"] - # and drop the duplicates ignoring the warning - with pd.option_context("mode.chained_assignment", None): - df.drop_duplicates(inplace=True) + # and drop the duplicates + dictionary[key] = df.drop_duplicates() except KeyError: pass # no user column @@ -195,17 +194,16 @@ def __init__( else: self._tally_comments.append("") - for df in tallydata.values(): + for key, df in tallydata.items(): # drop a row if it contains total in whatever column if "total" in df.values: - df.drop( + tallydata[key] = df.drop( df[ df.apply( lambda row: row.astype(str).str.contains("total").any(), axis=1, ) - ].index, - inplace=True, + ].index ) self._tallydata = tallydata diff --git a/tests/post/test_plotter.py b/tests/post/test_plotter.py index bde08258..446ea5b1 100644 --- a/tests/post/test_plotter.py +++ b/tests/post/test_plotter.py @@ -2,14 +2,23 @@ import random import string +import warnings +import matplotlib.pyplot as plt import numpy as np import pandas as pd import pytest from jade.config.atlas_config import PlotConfig from jade.helper.errors import PlotIndexMismatchError -from jade.post.plotter import BarPlot, BinnedPlot, CEPlot, WavesPlot +from jade.post.plotter import ( + BarPlot, + BinnedPlot, + CEPlot, + DoseContributionPlot, + WavesPlot, + _shorten_x_name, +) class TestBinnedPlot: @@ -265,3 +274,48 @@ def test_plot(self, tmpdir): plot = BarPlot(cfg, data) output = plot.plot() output[0][0].savefig(tmpdir.join("test.png"), bbox_inches="tight") + + +class TestDoseContributionPlot: + def test_plot(self, tmpdir): + cfg = PlotConfig( + name="test", + results=["a"], + plot_type=None, # dummy value + title="test", + x_label="Time", + y_labels=["dummy"], + x="Time", + y="Value", + ) + data = [] + for lib in ["lib0", "lib1"]: + df = pd.DataFrame( + { + "User": [11024, 11024, 27060, 27060], + "Time": [1, 2, 1, 2], + "Value": np.random.rand(4), + "Error": np.random.rand(4) * 0.1, + } + ) + data.append((lib, df)) + + plot = DoseContributionPlot(cfg, data) + fig, axes = plot.plot()[0] + fig.savefig(tmpdir.join("test.png")) + + # each subplot is titled after its own code-lib + assert [ax.get_title() for ax in axes] == ["lib0", "lib1"] + # the input data is not modified + for _, df in data: + assert "User" in df.columns + + +def test_shorten_x_name(): + fig, ax = plt.subplots() + ax.plot(["a_b_c", "d_e_f"], [1, 2]) + with warnings.catch_warnings(): + warnings.simplefilter("error") + _shorten_x_name(ax, 2) + assert [label.get_text() for label in ax.get_xticklabels()] == ["b c", "e f"] + plt.close(fig) From a3439b869f4a55b8f0adf9940bdb74e26f678c5d Mon Sep 17 00:00:00 2001 From: dodu94 Date: Tue, 27 Oct 2026 14:54:36 +0100 Subject: [PATCH 5/8] ruff and isort enforcement --- .github/workflows/lint.yml | 27 +++++++++ AGENTS.md | 4 +- docs/source/conf.py | 1 - src/jade/app/app.py | 8 +-- src/jade/app/fetch.py | 2 +- src/jade/config/atlas_config.py | 12 ++-- src/jade/config/excel_config.py | 2 +- src/jade/config/pp_config.py | 4 +- src/jade/config/raw_config.py | 2 +- src/jade/config/run_config.py | 1 - src/jade/config/status.py | 10 ++-- src/jade/helper/__optionals__.py | 10 ++-- src/jade/helper/aux_functions.py | 7 +-- src/jade/helper/openmc.py | 52 ++++++++-------- src/jade/post/atlas.py | 6 +- src/jade/post/excel_routines.py | 31 +++++----- src/jade/post/manipulate_tally.py | 4 +- src/jade/post/plotter.py | 18 +++--- src/jade/post/raw_processor.py | 2 +- src/jade/post/sim_output.py | 59 +++++-------------- src/jade/run/benchmark.py | 23 ++++---- src/jade/run/input.py | 8 +-- tests/app/test_app.py | 8 +-- tests/config/test_atlas_config.py | 2 +- tests/config/test_excel_config.py | 2 +- tests/config/test_raw_config.py | 4 +- tests/config/test_run_config.py | 2 +- tests/gui/test_config_gui.py | 6 +- tests/gui/test_post_config_gui.py | 8 +-- .../compatibility_scripts/convert_oktavian.py | 1 - ...ates.py => derive_iter1d_z_coordinates.py} | 0 tests/helper/test_auxiliary_func.py | 11 ++-- tests/helper/test_openmc.py | 3 +- tests/post/test_excel_routines.py | 9 ++- tests/post/test_sim_output.py | 12 +++- 35 files changed, 180 insertions(+), 181 deletions(-) create mode 100644 .github/workflows/lint.yml rename tests/helper/compatibility_scripts/{derive_ITER1D_z_coordinates.py => derive_iter1d_z_coordinates.py} (100%) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..ddacb30a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: lint + +# Controls when the workflow will run +on: [push, pull_request, workflow_dispatch] + +jobs: + ruff: + runs-on: ubuntu-latest + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + # Lint (includes import sorting). Ruff version is pinned so that the rule set + # does not change under our feet when a new ruff release adds default rules. + - name: Ruff check + uses: astral-sh/ruff-action@v3 + with: + version: "0.17.3" + args: "check" + + # Fail if any file is not formatted + - name: Ruff format + uses: astral-sh/ruff-action@v3 + with: + version: "0.17.3" + args: "format --check --diff" diff --git a/AGENTS.md b/AGENTS.md index aced5992..b638d67b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,8 +75,8 @@ The same pattern applies to `Library`/`LibraryFactory`. `Input` subclasses are s ### Config construction: always via named classmethods ```python -run_cfg = RunConfig.from_root(paths_tree) # reads 4 YAML files -run_cfg = RunConfig.from_yamls(env_path, ...) # explicit paths +run_cfg = RunConfig.from_root(paths_tree) # reads 4 YAML files +run_cfg = RunConfig.from_yamls(env_path, ...) # explicit paths ``` Never construct config dataclasses directly. diff --git a/docs/source/conf.py b/docs/source/conf.py index 6bb94bb1..614c80c6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,7 +14,6 @@ import sys sys.path.insert(0, os.path.abspath("../..")) -import importlib.metadata # -- Project information ----------------------------------------------------- diff --git a/src/jade/app/app.py b/src/jade/app/app.py index 68b787fc..4de99d21 100644 --- a/src/jade/app/app.py +++ b/src/jade/app/app.py @@ -18,7 +18,7 @@ from jade.config.raw_config import ConfigRawProcessor from jade.config.run_config import RunConfig, RunMode from jade.config.status import GlobalStatus -from jade.helper.__optionals__ import TKINTER_AVAIL, OMC_AVAIL +from jade.helper.__optionals__ import OMC_AVAIL, TKINTER_AVAIL if TKINTER_AVAIL: from jade.gui.post_config_gui import PostConfigGUI @@ -147,8 +147,8 @@ def restore_default_cfg(self, msg: str = ""): def rmv_runtpe(self): """Remove the runtpe files from the simulation folders of mcnp.""" # Walk through the root folder and remove the runtpe files - for pathroot, folder, files in os.walk(self.tree.simulations): - if len(files) > 0 and len(folder) == 0: # only files and no folders + for pathroot, folder, filenames in os.walk(self.tree.simulations): + if len(filenames) > 0 and len(folder) == 0: # only files and no folders # get the code codelib = Path(pathroot).parent.parent.name code, _ = get_code_lib(codelib) @@ -203,7 +203,7 @@ def run_benchmarks(self, testing: bool = False) -> list[str] | None: def continue_run(self, testing: bool = False): """Continue the run of the benchmarks that were not completed.""" commands = [] - for bench_name, cfg in self.run_cfg.benchmarks.items(): + for cfg in self.run_cfg.benchmarks.values(): benchmark = BenchmarkRunFactory.create( cfg, self.tree.simulations, diff --git a/src/jade/app/fetch.py b/src/jade/app/fetch.py index 24284f71..6f9b294f 100644 --- a/src/jade/app/fetch.py +++ b/src/jade/app/fetch.py @@ -71,7 +71,7 @@ def _extract_zip(zip_content, dest: PathLike) -> PathLike: def fetch_from_gitlab( - url: str, path: str, branch: str, authorization_token: str = None + url: str, path: str, branch: str, authorization_token: str | None = None ) -> PathLike | bool: """Download a repository from GitLab and extract it to a temporary folder. It can also deal with authentication. Supported diff --git a/src/jade/config/atlas_config.py b/src/jade/config/atlas_config.py index b9c3b9e7..f20ec748 100644 --- a/src/jade/config/atlas_config.py +++ b/src/jade/config/atlas_config.py @@ -160,23 +160,23 @@ def __post_init__(self): # Ensure correct format of additional labels if present if self.additional_labels is not None: try: - for key in self.additional_labels.keys(): + for key in self.additional_labels: assert key in ["major", "minor"] assert isinstance(self.additional_labels[key], list) for label, position in self.additional_labels[key]: assert isinstance(label, str) - assert isinstance(position, float) or isinstance(position, int) + assert isinstance(position, (float, int)) except AssertionError: raise ConfigError("Invalid format for 'additional_labels'") # Ensure correct format of v_lines if present if self.v_lines is not None: try: - for key in self.v_lines.keys(): + for key in self.v_lines: assert key in ["major", "minor"] assert isinstance(self.v_lines[key], list) for line in self.v_lines[key]: - assert isinstance(line, float) or isinstance(line, int) + assert isinstance(line, (float, int)) except AssertionError: raise ConfigError("Invalid format for 'v_lines'") @@ -187,8 +187,8 @@ def __post_init__(self): assert len(rec) == 4 assert isinstance(rec[0], str) assert isinstance(rec[1], str) - assert isinstance(rec[2], float) or isinstance(rec[2], int) - assert isinstance(rec[3], float) or isinstance(rec[3], int) + assert isinstance(rec[2], (float, int)) + assert isinstance(rec[3], (float, int)) except AssertionError: raise ConfigError("Invalid format for 'recs'") diff --git a/src/jade/config/excel_config.py b/src/jade/config/excel_config.py index 902d2d5c..18d4cb55 100644 --- a/src/jade/config/excel_config.py +++ b/src/jade/config/excel_config.py @@ -116,7 +116,7 @@ def __post_init__(self): if self.conditional_formatting: assert len(self.conditional_formatting) == 3 for key in ["red", "yellow", "orange"]: - assert key in self.conditional_formatting.keys() + assert key in self.conditional_formatting @classmethod def from_dict(cls, dictionary: dict, name) -> TableConfig: diff --git a/src/jade/config/pp_config.py b/src/jade/config/pp_config.py index 72f22a47..2a31016d 100644 --- a/src/jade/config/pp_config.py +++ b/src/jade/config/pp_config.py @@ -13,14 +13,14 @@ def __init__(self, root_cfg_pp: PathLike): # get all available config excel processors excel_cfgs = {} for file in os.listdir(Path(root_cfg_pp, "excel")): - if file.endswith(".yaml") or file.endswith(".yml"): + if file.endswith((".yaml", ".yml")): cfg = ConfigExcelProcessor.from_yaml(Path(root_cfg_pp, "excel", file)) excel_cfgs[cfg.benchmark] = cfg self.excel_cfgs = excel_cfgs # Get all available config atlas processors atlas_cfgs = {} for file in os.listdir(Path(root_cfg_pp, "atlas")): - if file.endswith(".yaml") or file.endswith(".yml"): + if file.endswith((".yaml", ".yml")): cfg = ConfigAtlasProcessor.from_yaml(Path(root_cfg_pp, "atlas", file)) atlas_cfgs[cfg.benchmark] = cfg self.atlas_cfgs = atlas_cfgs diff --git a/src/jade/config/raw_config.py b/src/jade/config/raw_config.py index 2808a19d..877bf012 100644 --- a/src/jade/config/raw_config.py +++ b/src/jade/config/raw_config.py @@ -1,12 +1,12 @@ from __future__ import annotations +import re from dataclasses import dataclass from enum import Enum import yaml from jade.helper.aux_functions import PathLike -import re APPLY_TO_TAG = re.compile(r"apply_to#\d+") diff --git a/src/jade/config/run_config.py b/src/jade/config/run_config.py index 8d9dd0b0..102cb526 100644 --- a/src/jade/config/run_config.py +++ b/src/jade/config/run_config.py @@ -248,7 +248,6 @@ def get_lib_zaids(self) -> list[str]: list[str] list of zaids available in the library. Zaid numbers to be used """ - pass @dataclass diff --git a/src/jade/config/status.py b/src/jade/config/status.py index 8526cf30..4a2d24be 100644 --- a/src/jade/config/status.py +++ b/src/jade/config/status.py @@ -199,7 +199,7 @@ def get_all_raw(self) -> tuple[set[str], set[str]]: """ code_libs = [] benchmarks = [] - for code, lib, benchmark in self.raw_data.keys(): + for code, lib, benchmark in self.raw_data: code_libs.append(print_code_lib(code, lib)) benchmarks.append(benchmark) return set(code_libs), set(benchmarks) @@ -222,7 +222,7 @@ def get_codelibs_from_raw_benchmark(self, benchmarks: str | list[str]) -> set[st benchmarks = [benchmarks] codelibs = [] - for code, lib, bench in self.raw_data.keys(): + for code, lib, bench in self.raw_data: if bench in benchmarks: codelibs.append(print_code_lib(code, lib)) return set(codelibs) @@ -242,7 +242,7 @@ def get_benchmark_from_raw_codelib(self, codelibs: str | list[str]) -> set[str]: list of benchmarks for which the raw data of the requested codelib is available. """ benchmarks = [] - for code, lib, bench in self.raw_data.keys(): + for code, lib, bench in self.raw_data: if print_code_lib(code, lib) in codelibs: benchmarks.append(bench) return set(benchmarks) @@ -262,9 +262,7 @@ def is_raw_available(self, codelib: str, benchmark: str) -> bool: bool True if the raw data is available, False otherwise. """ - if codelib in self.get_codelibs_from_raw_benchmark(benchmark): - return True - return False + return codelib in self.get_codelibs_from_raw_benchmark(benchmark) def _validate_libs_processing( self, code: CODE, benchmark: str, libs: list[str] diff --git a/src/jade/helper/__optionals__.py b/src/jade/helper/__optionals__.py index be758274..413a3d04 100644 --- a/src/jade/helper/__optionals__.py +++ b/src/jade/helper/__optionals__.py @@ -4,19 +4,17 @@ # Handle OpenMC optional dependency with global flag. try: - import openmc + import openmc # noqa: F401 OMC_AVAIL = True except ImportError: OMC_AVAIL = False - logger.warning( - "OpenMC has not been installed - see JADE installation instructions" - ) + logger.warning("OpenMC has not been installed - see JADE installation instructions") try: - import tkinter as tk + import tkinter as tk # noqa: F401 - import ttkthemes + import ttkthemes # noqa: F401 TKINTER_AVAIL = True except ImportError: diff --git a/src/jade/helper/aux_functions.py b/src/jade/helper/aux_functions.py index a89ebc1c..2286388c 100644 --- a/src/jade/helper/aux_functions.py +++ b/src/jade/helper/aux_functions.py @@ -5,7 +5,7 @@ import re from abc import ABC, abstractmethod from pathlib import Path -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING import numpy as np import pandas as pd @@ -16,7 +16,7 @@ from jade.helper.constants import CODE from jade.post.sim_output import MCNPSimOutput, OpenMCSimOutput -PathLike = Union[str, os.PathLike, Path] +PathLike = str | os.PathLike | Path CODE_PATTERN = re.compile(r"_.+_-") LIB_PATTERN = re.compile(r"-_.+_") @@ -96,7 +96,6 @@ def check_success(self, files: list[str]) -> bool: bool True if the simulation completed successfully, False otherwise. """ - pass class MCNPChecker(SimulationChecker): @@ -208,6 +207,6 @@ def same_index( continue else: return False - except Exception: + except (ValueError, TypeError): return False return True diff --git a/src/jade/helper/openmc.py b/src/jade/helper/openmc.py index 9fcdfe70..6683fe00 100644 --- a/src/jade/helper/openmc.py +++ b/src/jade/helper/openmc.py @@ -107,9 +107,9 @@ def from_volume_density( cell_volumes: dict[int, float], cell_densities: dict[int, float] ) -> dict[int, float]: cell_masses = {} - for cell in cell_volumes: + for cell, volume in cell_volumes.items(): if cell in cell_densities: - cell_masses[cell] = cell_volumes[cell] * cell_densities[cell] + cell_masses[cell] = volume * cell_densities[cell] return cell_masses def volumes(self, cells: iter | None = None) -> dict[int, float]: @@ -121,9 +121,7 @@ def volumes(self, cells: iter | None = None) -> dict[int, float]: volumes : dict Dictionary of volumes, indexed by cell number """ - volumes = dict( - (k, self.cell_volumes[k]) for k in cells if k in self.cell_volumes - ) + volumes = {k: self.cell_volumes[k] for k in cells if k in self.cell_volumes} return volumes def densities(self, cells: iter | None = None) -> dict[int, float]: @@ -135,9 +133,9 @@ def densities(self, cells: iter | None = None) -> dict[int, float]: densities : dict Dictionary of densities, indexed by cell number """ - densities = dict( - (k, self.cell_densities[k]) for k in cells if k in self.cell_densities - ) + densities = { + k: self.cell_densities[k] for k in cells if k in self.cell_densities + } return densities def masses(self, cells: iter | None = None) -> dict[int, float]: @@ -149,7 +147,7 @@ def masses(self, cells: iter | None = None) -> dict[int, float]: masses : dict Dictionary of masses, indexed by cell number """ - masses = dict((k, self.cell_masses[k]) for k in cells if k in self.cell_masses) + masses = {k: self.cell_masses[k] for k in cells if k in self.cell_masses} return masses @@ -279,8 +277,7 @@ def add_stopCard(self, nps: int, batches: int = 100) -> None: ------- None """ - if nps < batches: - batches = nps + batches = min(batches, nps) self.settings.batches = batches self.settings.particles = int(nps / batches) @@ -436,7 +433,7 @@ def initialise( self.statepoint = openmc.StatePoint(spfile_path) self.tally_numbers = [] self.tally_comments = [] - for _, tally in self.statepoint.tallies.items(): + for tally in self.statepoint.tallies.values(): self.tally_numbers.append(tally.id) self.tally_comments.append(tally.name) except (FileNotFoundError, KeyError): @@ -555,24 +552,27 @@ def _combine_heating_tallies(self, heating_tallies: dict) -> dict: heating_tallies_df[id] = self._get_tally_data(tally) for id, photon_tally in photon_tallies.items(): photon_cell_filter = photon_tally.find_filter(openmc.CellFilter) - for _, tally in heating_tallies.items(): + for tally in heating_tallies.values(): try: particle_filter = tally.find_filter(openmc.ParticleFilter) except ValueError: particle_filter = None cell_filter = tally.find_filter(openmc.CellFilter) - if particle_filter and cell_filter: - if photon_cell_filter == cell_filter: - if ( - "electron" in particle_filter.bins - or "positron" in particle_filter.bins - ): - tally_df = self._get_tally_data(tally) - heating_tallies_df[id]["mean"] += tally_df["mean"] - heating_tallies_df[id]["std. dev."] = ( - heating_tallies_df[id]["std. dev."].pow(2) - + tally_df["std. dev."].pow(2) - ).pow(0.5) + if ( + particle_filter + and cell_filter + and photon_cell_filter == cell_filter + and ( + "electron" in particle_filter.bins + or "positron" in particle_filter.bins + ) + ): + tally_df = self._get_tally_data(tally) + heating_tallies_df[id]["mean"] += tally_df["mean"] + heating_tallies_df[id]["std. dev."] = ( + heating_tallies_df[id]["std. dev."].pow(2) + + tally_df["std. dev."].pow(2) + ).pow(0.5) return heating_tallies_df def tallies_to_dataframes(self) -> dict: @@ -585,7 +585,7 @@ def tallies_to_dataframes(self) -> dict: """ tallies = {} heating_tallies = {} - for _, tally in self.statepoint.tallies.items(): + for tally in self.statepoint.tallies.values(): if "heating" in tally.scores: heating_tallies[tally.id] = tally else: diff --git a/src/jade/post/atlas.py b/src/jade/post/atlas.py index 6c6b970d..d56b67d2 100644 --- a/src/jade/post/atlas.py +++ b/src/jade/post/atlas.py @@ -5,6 +5,7 @@ # import win32com.client # import aspose.words import docx +import matplotlib.pyplot as plt # from docx.enum.table import WD_ALIGN_VERTICAL from docx.enum.text import WD_ALIGN_PARAGRAPH @@ -13,10 +14,11 @@ # from docx.oxml.ns import nsdecls, qn from docx.shared import Inches from matplotlib.figure import Figure -import matplotlib.pyplot as plt from jade.helper.aux_functions import PathLike +DEFAULT_IMG_WIDTH = Inches(7.5) + logger = logging.getLogger(__name__) @@ -44,7 +46,7 @@ def __init__(self, template: PathLike, name: str): self.outname = "atlas_" + name # Name for the outfile self.doc = doc # Word Document - def insert_img(self, figure: Figure, width=Inches(7.5)): + def insert_img(self, figure: Figure, width=DEFAULT_IMG_WIDTH): """Insert an image in the word document Parameters diff --git a/src/jade/post/excel_routines.py b/src/jade/post/excel_routines.py index b4fa915c..40d1c1c7 100644 --- a/src/jade/post/excel_routines.py +++ b/src/jade/post/excel_routines.py @@ -157,7 +157,6 @@ def add_sheets(self): def _get_sheet(self) -> list[pd.DataFrame]: """this is the core of the table, its data. It should return a list of dataframes that will be added to the workbook (one per sheet).""" - pass @staticmethod def _rename_columns(df: pd.DataFrame, names: dict[str, str]) -> None: @@ -251,7 +250,7 @@ def _get_sheet(self) -> list[pd.DataFrame]: class SpherePivotTable(PivotTable): """Pivot table with custom sorting for Sphere benchmark. - + Sorts the index by: 1. Isotopes first (sorted by Z number extracted from the case name) 2. Materials second (sorted alphabetically) @@ -260,27 +259,27 @@ class SpherePivotTable(PivotTable): def _get_sheet(self) -> list[pd.DataFrame]: # Get the parent class results dfs = super()._get_sheet() - + # Apply sorting to all dataframes sorted_dfs = [] for df in dfs: sorted_df = self._sort_sphere_index(df) sorted_dfs.append(sorted_df) - + return sorted_dfs @staticmethod def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: """Sort the index of the dataframe for Sphere benchmark. - + Isotopes (cases starting with numbers) are sorted by their Z number, and materials (cases starting with 'M') are sorted alphabetically. - + Parameters ---------- df : pd.DataFrame DataFrame with index to sort - + Returns ------- pd.DataFrame @@ -288,7 +287,7 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: """ if df.empty: return df - + # Get the index name(s) if isinstance(df.index, pd.MultiIndex): # For multi-index, we need to sort by the first level (Case) @@ -297,16 +296,16 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: else: # For single index cases = df.index.unique() - + # Separate isotopes and materials isotopes = [] materials = [] - + for case in cases: case_str = str(case) # Check if it starts with a digit (isotope) or 'M' (material) # Expected format: "Sphere_1001_H-1" for isotopes or "Sphere_M101" for materials - parts = case_str.split('_') + parts = case_str.split("_") if len(parts) >= 2: identifier = parts[1] # e.g., "1001" or "M101" if identifier[0].isdigit(): @@ -321,7 +320,7 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: # Unknown format, use the identifier as-is for sorting z_number = int(identifier) if identifier.isdigit() else 0 isotopes.append((z_number, case)) - elif identifier.startswith('M'): + elif identifier.startswith("M"): materials.append(case) else: # Unknown format, treat as material @@ -329,14 +328,14 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: else: # Unknown format, treat as material materials.append(case) - + # Sort isotopes by Z number, materials alphabetically isotopes.sort(key=lambda x: x[0]) materials.sort() - + # Create the sorted order sorted_order = [case for _, case in isotopes] + materials - + # Reindex the dataframe if isinstance(df.index, pd.MultiIndex): # For multi-index, we need to reindex by the first level @@ -351,7 +350,7 @@ def _sort_sphere_index(df: pd.DataFrame) -> pd.DataFrame: else: # Simple reindex for single index df = df.reindex(sorted_order) - + return df diff --git a/src/jade/post/manipulate_tally.py b/src/jade/post/manipulate_tally.py index 8b405525..174e8ebd 100644 --- a/src/jade/post/manipulate_tally.py +++ b/src/jade/post/manipulate_tally.py @@ -123,7 +123,7 @@ def condense_groups( def scale( - tally: pd.DataFrame, factor: int | float | list = 1, column: str = "Value" + tally: pd.DataFrame, factor: float | list = 1, column: str = "Value" ) -> pd.DataFrame: """Scale the tally values.""" if isinstance(factor, list): @@ -357,7 +357,7 @@ def gaussian_broadening( "Length of fwhm_frac list must match number of rows in tally." ) else: - raise ValueError("fwhm_frac must be a float or a list of floats.") + raise TypeError("fwhm_frac must be a float or a list of floats.") Eb = tally["Energy"].values.astype(float) Yb = np.zeros_like(tally["Value"]) diff --git a/src/jade/post/plotter.py b/src/jade/post/plotter.py index 25ffd726..13d44d21 100644 --- a/src/jade/post/plotter.py +++ b/src/jade/post/plotter.py @@ -158,7 +158,7 @@ def _add_vlines(self, ax: Axes) -> None: if self.cfg.v_lines is None: return - if "major" in self.cfg.v_lines.keys(): + if "major" in self.cfg.v_lines: lines = self.cfg.v_lines["major"] for line in lines: # Check first if the value is in the axis limits @@ -166,7 +166,7 @@ def _add_vlines(self, ax: Axes) -> None: ax.axvline(line, color="black") # minor lines - if "minor" in self.cfg.v_lines.keys(): + if "minor" in self.cfg.v_lines: lines = self.cfg.v_lines["minor"] for line in lines: # Check first if the value is in the axis limits @@ -186,7 +186,7 @@ def _add_labels(self, ax: Axes) -> None: if self.cfg.additional_labels is None: return - if "major" in self.cfg.additional_labels.keys(): + if "major" in self.cfg.additional_labels: labels = self.cfg.additional_labels["major"] bbox_dic = { "boxstyle": "round,pad=0.5", @@ -195,7 +195,7 @@ def _add_labels(self, ax: Axes) -> None: } _place_labels(labels, ax, 0.93, bbox=bbox_dic) - if "minor" in self.cfg.additional_labels.keys(): + if "minor" in self.cfg.additional_labels: # minor labels labels = self.cfg.additional_labels["minor"] _place_labels(labels, ax, 0.87) @@ -204,7 +204,7 @@ def _add_labels(self, ax: Axes) -> None: def _place_labels( labels: list[tuple[str, float | int]], ax: Axes, - ypos_perc: float | int, + ypos_perc: float, bbox: dict | None = None, ) -> None: """Helper function to place labels on the correct position in the plot""" @@ -1031,8 +1031,8 @@ def create_plot( # Aux functions def _get_limits( - lowerlimit: float | int, - upperlimit: float | int, + lowerlimit: float, + upperlimit: float, ydata: list | np.ndarray, xdata: list | np.ndarray, ): @@ -1095,8 +1095,8 @@ def _get_error_x_pos(x_array: np.ndarray) -> np.ndarray: def _apply_CE_limits( - min: float | int, - max: float | int, + min: float, + max: float, ydata: np.ndarray, xdata: np.ndarray, ax: Axes, diff --git a/src/jade/post/raw_processor.py b/src/jade/post/raw_processor.py index 8a620605..1bf5baae 100644 --- a/src/jade/post/raw_processor.py +++ b/src/jade/post/raw_processor.py @@ -59,7 +59,7 @@ def __init__( if self.code in (CODE.MCNP, CODE.D1S): self.sim_output = MCNPSimOutput(sim_folder) elif self.code == CODE.OPENMC: - if self.metadata.get("benchmark_name") =="Sphere": + if self.metadata.get("benchmark_name") == "Sphere": self.sim_output = OpenMCSphereSimOutput(sim_folder) else: self.sim_output = OpenMCSimOutput(sim_folder) diff --git a/src/jade/post/sim_output.py b/src/jade/post/sim_output.py index e5bb0d70..1ab368d2 100644 --- a/src/jade/post/sim_output.py +++ b/src/jade/post/sim_output.py @@ -49,7 +49,6 @@ def tallydata(self) -> dict[int, pd.DataFrame]: dict[str, pd.DataFrame] Dictionary of tally dataframes, indexed by tally number. """ - pass @property @abstractmethod @@ -73,7 +72,6 @@ def tally_numbers(self) -> list[int]: list[int] List of tally numbers. """ - pass @property @abstractmethod @@ -85,7 +83,6 @@ def tally_comments(self) -> list[str]: list[str] List of tally comments. """ - pass @abstractmethod def _read_code_version(self) -> str: @@ -96,7 +93,6 @@ def _read_code_version(self) -> str: str The code version. """ - pass class MCNPSimOutput(AbstractSimOutput): @@ -327,7 +323,6 @@ def retrieve_file( file1 = None file2 = None file3 = None - file4 = None for file_name in os.listdir(results_path): if file_name.endswith(".out"): @@ -356,11 +351,13 @@ def is_successfully_simulated(files: list[str]) -> bool: if file.startswith("statepoint") and file.endswith(".h5"): statepoint_found = True return statepoint_found - - def _prep_tally(self, filter_lookup: dict[str, str], tally: pd.DataFrame) -> pd.DataFrame: - ''' + + def _prep_tally( + self, filter_lookup: dict[str, str], tally: pd.DataFrame + ) -> pd.DataFrame: + """ Function to prepare the tally dataframe for JADE formatting, by renaming the columns and sorting by the filters. - + Parameters ---------- filter_lookup : dict @@ -373,7 +370,7 @@ def _prep_tally(self, filter_lookup: dict[str, str], tally: pd.DataFrame) -> pd. sorted_tally : pd.DataFrame The sorted and renamed tally dataframe ready for JADE formatting - ''' + """ filters = [] new_columns = {} if "cell" in tally.columns: @@ -384,8 +381,7 @@ def _prep_tally(self, filter_lookup: dict[str, str], tally: pd.DataFrame) -> pd. filters.append("energy high [eV]") if "time" in tally.columns: filters.append("time") - new_columns = dict( - (k, filter_lookup[k]) for k in filters if k in filter_lookup) + new_columns = {k: filter_lookup[k] for k in filters if k in filter_lookup} new_columns["mean"] = filter_lookup["mean"] new_columns["std. dev."] = filter_lookup["std. dev."] sorted_tally = tally.sort_values(filters) @@ -423,25 +419,12 @@ def _create_dataframes( "mean": "Value", "std. dev.": "Error", } - columns = [ - "Cells", - "User", - "Segments", - "Cosine", - "Energy", - "Time", - "Cor C", - "Cor B", - "Cor A", - "Value", - "Error", - ] for id, tally in tallies.items(): sorted_tally = self._prep_tally(filter_lookup, tally) - + if "Value" in sorted_tally.columns and "Error" in sorted_tally.columns: sorted_tally["Error"] = sorted_tally["Error"] / sorted_tally["Value"] - + tallydata[id] = sorted_tally totalbin[id] = None return tallydata, totalbin @@ -483,8 +466,8 @@ def __init__( None. """ - _, statefile, volfile = self.retrieve_file(sim_folder) - + self.retrieve_file(sim_folder) + # Retrieving atomic density for normalisation of the DPA, He and T production tallies self.input = omc.OpenMCInputFiles(sim_folder) materials = self.input.geometry.get_all_materials() @@ -522,19 +505,6 @@ def _create_dataframes( "mean": "Value", "std. dev.": "Error", } - columns = [ - "Cells", - "User", - "Segments", - "Cosine", - "Energy", - "Time", - "Cor C", - "Cor B", - "Cor A", - "Value", - "Error", - ] for id, tally in tallies.items(): sorted_tally = self._prep_tally(filter_lookup, tally) @@ -546,14 +516,15 @@ def _create_dataframes( sorted_tally["Error"] = sorted_tally["Error"] / self.atomic_density else: pass - + if "Value" in sorted_tally.columns and "Error" in sorted_tally.columns: sorted_tally["Error"] = sorted_tally["Error"] / sorted_tally["Value"] - + tallydata[id] = sorted_tally totalbin[id] = None return tallydata, totalbin + def _remove_constant_columns(df: pd.DataFrame) -> pd.DataFrame: """eliminate unnecessary columns from OpenMC tally data""" # Drop constant axes (if len is > 1) diff --git a/src/jade/run/benchmark.py b/src/jade/run/benchmark.py index cc656985..a92e7fb3 100644 --- a/src/jade/run/benchmark.py +++ b/src/jade/run/benchmark.py @@ -96,7 +96,6 @@ def _build_command(self, config: EnvironmentVariables) -> list[str]: """ # This method needs to build the command to run the code, will be different # depending on codes - pass @abstractmethod def _get_lib_data_command(self) -> tuple[str, str]: @@ -252,9 +251,6 @@ def _submit_job( """ # store cwd to get back to it after job submission cwd = os.getcwd() - user = ( - subprocess.run("whoami", capture_output=True).stdout.decode("utf-8").strip() - ) os.chdir(directory) job_script = os.path.join( directory, os.path.basename(directory) + "_job_script" @@ -275,7 +271,9 @@ def _submit_job( # Submit the job using the specified batch system (checked for existence in post_init) if not test: - subprocess.run([env_vars.scheduler_command, job_script], cwd=directory) + subprocess.run( + [env_vars.scheduler_command, job_script], cwd=directory, check=False + ) # return to the original directory os.chdir(cwd) @@ -582,7 +580,7 @@ def _get_continue_run_command( # if MCNP or similar, check for output file and remove if found if code == CODE.MCNP or code == CODE.D1S: for file in os.listdir(single_run_root): - if file.endswith(".o") or file.endswith(".r"): + if file.endswith((".o", ".r")): os.remove(Path(single_run_root, file)) # create the single run using the mock input @@ -1038,8 +1036,11 @@ def replace_template_vars( """ job_template = env_vars.code_job_template[code] - # store cwd to get back to it after job submission - user = subprocess.run("whoami", capture_output=True).stdout.decode("utf-8").strip() + user = ( + subprocess.run("whoami", capture_output=True, check=False) + .stdout.decode("utf-8") + .strip() + ) job_script = os.path.join(directory, os.path.basename(directory) + "_job_script") if not os.path.isfile(job_template): @@ -1049,7 +1050,7 @@ def replace_template_vars( with open(job_template) as fin: # Replace placeholders in batch file template with actual values contents = fin.read() - contents = contents.replace("INITIAL_DIR", f'"{str(directory)}"') + contents = contents.replace("INITIAL_DIR", f'"{directory!s}"') contents = contents.replace("OUT_FILE", f'"{job_script + ".out"}"') contents = contents.replace("ERROR_FILE", f'"{job_script + ".err"}"') contents = contents.replace("MPI_TASKS", str(env_vars.mpi_tasks)) @@ -1106,7 +1107,9 @@ def launch_global_jobs( if not test: with open(job_script, "w") as fout: fout.write(contents) - subprocess.run([env_vars.scheduler_command, job_script], cwd=cwd) + subprocess.run( + [env_vars.scheduler_command, job_script], cwd=cwd, check=False + ) # return to the original directory os.chdir(cwd) return scripts diff --git a/src/jade/run/input.py b/src/jade/run/input.py index 2b139f0a..aa9d3965 100644 --- a/src/jade/run/input.py +++ b/src/jade/run/input.py @@ -8,12 +8,12 @@ from copy import deepcopy from pathlib import Path +from f4enix.core.irradiation import Nuclide from f4enix.input.d1suned import IrradiationFile, Reaction, ReactionFile from f4enix.input.libmanager import LibManager from f4enix.input.materials import MatCardsList, Material, Zaid from f4enix.input.MCNPinput import D1S_Input from f4enix.input.MCNPinput import Input as MCNPInput -from f4enix.core.irradiation import Nuclide from jade.config.run_config import Library, LibraryACTINV, LibraryD1S, LibraryMCNP from jade.helper.__optionals__ import OMC_AVAIL @@ -36,19 +36,16 @@ def __init__(self, template_folder: PathLike, lib: Library): @abstractmethod def code(self) -> CODE: """Code of the input file.""" - pass @property @abstractmethod def name(self) -> str: """Name of the input file. E.g. ITER_1D""" - pass @property @abstractmethod def lib(self) -> Library: """Library used for the input file.""" - pass @abstractmethod def set_nps(self, nps: int): @@ -59,7 +56,6 @@ def set_nps(self, nps: int): nps : int number of particles to simulate. """ - pass @abstractmethod def translate(self): @@ -70,13 +66,11 @@ def translate(self): lib : Library library to which the input needs to be translated to. """ - pass @abstractmethod def _write(self, output_folder: PathLike): """Write the input files to the output folder. This should handle also additional files such as the wwinp for MCNP.""" - pass def write(self, output_folder: PathLike): """Write the input files to the output folder. Specific files are hendled diff --git a/tests/app/test_app.py b/tests/app/test_app.py index c3edf2f3..e5765d21 100644 --- a/tests/app/test_app.py +++ b/tests/app/test_app.py @@ -5,8 +5,6 @@ from importlib.resources import files from pathlib import Path -import pytest - import tests from jade.app.app import JadeApp from jade.config.run_config import ( @@ -228,9 +226,9 @@ def test_continue_run_sphere(self, tmp_path): assert "Sphere_dummy1" in command_sorted[0][1] assert "Sphere_m101" in command_sorted[1][1] - #assert "#!/bin/sh\n\n#SBATCH" in command[0][1] - #assert "Sphere_dummy1" in command[1][1] - #assert "Sphere_m101" in command[0][1] + # assert "#!/bin/sh\n\n#SBATCH" in command[0][1] + # assert "Sphere_dummy1" in command[1][1] + # assert "Sphere_m101" in command[0][1] app.run_cfg.env_vars.run_mode = RunMode.GLOBAL_JOB command = app.continue_run(testing=True) diff --git a/tests/config/test_atlas_config.py b/tests/config/test_atlas_config.py index de37fee2..58cfa5d4 100644 --- a/tests/config/test_atlas_config.py +++ b/tests/config/test_atlas_config.py @@ -12,5 +12,5 @@ class TestConfigAtlasProcessor: def test_all_default_files(self): for path in os.listdir(EXCEL_ROOT): - if path.endswith(".yml") or path.endswith(".yaml"): + if path.endswith((".yml", ".yaml")): ConfigAtlasProcessor.from_yaml(EXCEL_ROOT.joinpath(path)) diff --git a/tests/config/test_excel_config.py b/tests/config/test_excel_config.py index 69cb6600..e51c275a 100644 --- a/tests/config/test_excel_config.py +++ b/tests/config/test_excel_config.py @@ -12,5 +12,5 @@ class TestConfigExcelProcessor: def test_all_default_files(self): for path in os.listdir(EXCEL_ROOT): - if path.endswith(".yml") or path.endswith(".yaml"): + if path.endswith((".yml", ".yaml")): ConfigExcelProcessor.from_yaml(EXCEL_ROOT.joinpath(path)) diff --git a/tests/config/test_raw_config.py b/tests/config/test_raw_config.py index 2c3d7007..c9a04d00 100644 --- a/tests/config/test_raw_config.py +++ b/tests/config/test_raw_config.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from importlib.resources import files, as_file +from importlib.resources import as_file, files from pathlib import Path import jade.resources.default_cfg.benchmarks_pp as res @@ -36,7 +36,7 @@ def test_from_yaml(self): for code in os.listdir(CFG_ROOT): code_folder = Path(CFG_ROOT, code) for file in os.listdir(code_folder): - cfg = ConfigRawProcessor.from_yaml(Path(code_folder, file)) + ConfigRawProcessor.from_yaml(Path(code_folder, file)) def test_aliases(self): file = Path(CFG_ROOT, "mcnp", "ITER_1D.yaml") diff --git a/tests/config/test_run_config.py b/tests/config/test_run_config.py index 3b9fd09b..daac9938 100644 --- a/tests/config/test_run_config.py +++ b/tests/config/test_run_config.py @@ -84,7 +84,7 @@ def test_from_yamls(self, tmpdir): run_cfg_file = paths_tree.cfg.run_cfg lib_cfg_file = paths_tree.cfg.libs_cfg additional_settings_root = paths_tree.cfg.bench_additional_files - run_cfg = RunConfig.from_yamls( + RunConfig.from_yamls( env_vars_file, paths_tree.cfg.exe_cfg, run_cfg_file, diff --git a/tests/gui/test_config_gui.py b/tests/gui/test_config_gui.py index fa236992..e94a4c93 100644 --- a/tests/gui/test_config_gui.py +++ b/tests/gui/test_config_gui.py @@ -2,22 +2,24 @@ from importlib.resources import files from pathlib import Path +from unittest.mock import patch import pytest import jade.resources -from unittest.mock import patch - from jade.helper.__optionals__ import TKINTER_AVAIL if TKINTER_AVAIL: from tkinter import filedialog, messagebox, ttk + from ttkthemes import ThemedTk + from jade.gui.run_config_gui import ConfigGUI DEFAULT_CFG = files(jade.resources).joinpath("default_cfg") + @pytest.mark.skipif(not TKINTER_AVAIL, reason="tkinter is not available") class TestConfigGui: @pytest.fixture(scope="session") diff --git a/tests/gui/test_post_config_gui.py b/tests/gui/test_post_config_gui.py index 51fb6a63..a15bcc1b 100644 --- a/tests/gui/test_post_config_gui.py +++ b/tests/gui/test_post_config_gui.py @@ -3,13 +3,12 @@ from importlib.resources import files from unittest.mock import patch -import jade.resources -from jade.config.status import GlobalStatus -from tests import dummy_structure - import pytest +import jade.resources +from jade.config.status import GlobalStatus from jade.helper.__optionals__ import TKINTER_AVAIL +from tests import dummy_structure if TKINTER_AVAIL: from jade.gui.post_config_gui import PostConfigGUI @@ -18,6 +17,7 @@ DEFAULT_CFG = files(jade.resources).joinpath("default_cfg") + @pytest.mark.skipif(not TKINTER_AVAIL, reason="tkinter is not available") class TestPostConfigGui: def test_init(self): diff --git a/tests/helper/compatibility_scripts/convert_oktavian.py b/tests/helper/compatibility_scripts/convert_oktavian.py index 8e1567c3..ea9ace70 100644 --- a/tests/helper/compatibility_scripts/convert_oktavian.py +++ b/tests/helper/compatibility_scripts/convert_oktavian.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import shutil from pathlib import Path import pandas as pd diff --git a/tests/helper/compatibility_scripts/derive_ITER1D_z_coordinates.py b/tests/helper/compatibility_scripts/derive_iter1d_z_coordinates.py similarity index 100% rename from tests/helper/compatibility_scripts/derive_ITER1D_z_coordinates.py rename to tests/helper/compatibility_scripts/derive_iter1d_z_coordinates.py diff --git a/tests/helper/test_auxiliary_func.py b/tests/helper/test_auxiliary_func.py index e053748a..efe71b13 100644 --- a/tests/helper/test_auxiliary_func.py +++ b/tests/helper/test_auxiliary_func.py @@ -1,19 +1,20 @@ from __future__ import annotations +import os +import shutil from importlib.resources import as_file, files from pathlib import Path -from f4enix import Input + import pandas as pd -import os +from f4enix import Input from jade.config.run_config import LibraryOpenMC from jade.helper.aux_functions import ( + add_rmode0, get_code_lib, print_code_lib, same_index, - add_rmode0, ) -import shutil from jade.helper.constants import CODE from tests.config import resources as conf_res from tests.helper import res as helper_res @@ -55,6 +56,6 @@ def test_add_rmode0(tmp_path: Path): for file in os.listdir(dst): if file.endswith(".i"): inp = Input.from_input(dst.joinpath(file)) - assert "RMODE" in inp.other_data.keys(), ( + assert "RMODE" in inp.other_data, ( f"RMODE card not found in the input file {file}" ) diff --git a/tests/helper/test_openmc.py b/tests/helper/test_openmc.py index c433fb82..c5e2676e 100644 --- a/tests/helper/test_openmc.py +++ b/tests/helper/test_openmc.py @@ -1,8 +1,9 @@ from __future__ import annotations +import os from importlib.resources import files -import pytest, os +import pytest from jade.helper.__optionals__ import OMC_AVAIL from tests.post.resources import openmc as resources diff --git a/tests/post/test_excel_routines.py b/tests/post/test_excel_routines.py index c92c806b..63078a98 100644 --- a/tests/post/test_excel_routines.py +++ b/tests/post/test_excel_routines.py @@ -1,3 +1,5 @@ +from typing import ClassVar + import numpy as np import pandas as pd import pytest @@ -167,7 +169,8 @@ def test_sort_sphere_index_empty(self): def test_get_sheet_integration(self): """Test the full _get_sheet method with sorting applied""" import tempfile - from jade.config.excel_config import ComparisonType, TableConfig + + from jade.config.excel_config import ComparisonType # Create test data data = { @@ -186,8 +189,8 @@ def test_get_sheet_integration(self): # Create a mock config class MockConfig: - x = ["Case"] - y = ["Energy"] + x: ClassVar[list[str]] = ["Case"] + y: ClassVar[list[str]] = ["Energy"] value = "Value" add_error = False comparison_type = ComparisonType.PERCENTAGE diff --git a/tests/post/test_sim_output.py b/tests/post/test_sim_output.py index a7cc866d..ff8c35a0 100644 --- a/tests/post/test_sim_output.py +++ b/tests/post/test_sim_output.py @@ -8,7 +8,7 @@ import tests.dummy_structure as dummy_struct from jade.helper.__optionals__ import OMC_AVAIL -from jade.post.sim_output import MCNPSimOutput, OpenMCSimOutput, OpenMCSphereSimOutput +from jade.post.sim_output import MCNPSimOutput, OpenMCSphereSimOutput SIMULATION_FOLDER = files(dummy_struct).joinpath("simulations") @@ -45,24 +45,30 @@ def test_mcnp_tally_comments(self, mcnp_sim_output): isinstance(comment, str) for comment in mcnp_sim_output.tally_comments ) + @pytest.fixture def openmc_sphere_sim_output() -> OpenMCSphereSimOutput: folder = Path(SIMULATION_FOLDER, "_openmc_-_FENDL 3.2b_", "Sphere", "Sphere_m101") # Create dummy files for OpenMC Sphere return OpenMCSphereSimOutput(folder) + @pytest.mark.skipif(not OMC_AVAIL, reason="OpenMC is not available") class TestOpenMCSimoutput: pass + @pytest.mark.skipif(not OMC_AVAIL, reason="OpenMC is not available") class TestOpenMCSphereSimOutput: def test_openmc_sphere_tallydata(self, openmc_sphere_sim_output): assert isinstance(openmc_sphere_sim_output.tallydata, dict) assert all( - isinstance(df, pd.DataFrame) for df in openmc_sphere_sim_output.tallydata.values() + isinstance(df, pd.DataFrame) + for df in openmc_sphere_sim_output.tallydata.values() ) def test_atomic_density(self, openmc_sphere_sim_output): assert isinstance(openmc_sphere_sim_output.atomic_density, float) - assert openmc_sphere_sim_output.atomic_density == pytest.approx(0.042305713026896896) + assert openmc_sphere_sim_output.atomic_density == pytest.approx( + 0.042305713026896896 + ) From 9a9f8a50cdc4a8f240b04e1ee311f29c05846742 Mon Sep 17 00:00:00 2001 From: Davide Laghi Date: Tue, 27 Oct 2026 15:04:06 +0100 Subject: [PATCH 6/8] Update dependencies in pyproject.toml Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index bb64c353..893ccf9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ ] dependencies = [ "xlsxwriter < 4", + "python-docx < 2", "matplotlib < 4", "requests < 3", "f4enix >= 1.2.1, < 2", From f02258ff1bc64a11ff8c94889540124cf91bd336 Mon Sep 17 00:00:00 2001 From: Davide Laghi Date: Tue, 27 Oct 2026 15:04:51 +0100 Subject: [PATCH 7/8] Update dependencies in pyproject.toml Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 893ccf9b..910a0642 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,8 @@ docs = [ "sphinx-book-theme < 2", "sphinx-copybutton < 0.6", "sphinx-togglebutton < 0.5", - "sphinx-design < 0.8" + "sphinx-design < 0.8", + "pydata-sphinx-theme < 1", ] ui = [ "ttkthemes < 4" From caa2b76adc6a81edf0f6eb727cb3f9b74c369e06 Mon Sep 17 00:00:00 2001 From: Davide Laghi Date: Tue, 27 Oct 2026 15:05:30 +0100 Subject: [PATCH 8/8] Handle GitlabConnectionError in fetch.py Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/jade/app/fetch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jade/app/fetch.py b/src/jade/app/fetch.py index 6f9b294f..006953e1 100644 --- a/src/jade/app/fetch.py +++ b/src/jade/app/fetch.py @@ -103,7 +103,7 @@ def fetch_from_gitlab( logger.error("Gitlab authentication failed") logger.error(e) return False - except requests.exceptions.RequestException as e: + except gitlab.exceptions.GitlabConnectionError as e: logger.error(f"Could not connect to {url}") logger.error(e) return False