From 408a97281b04954acbfa2f3ba4b32fc2abb382d2 Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Wed, 11 Mar 2026 14:52:35 -0700 Subject: [PATCH 01/12] Made import of tkinter optional. --- pyyeti/guitools.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pyyeti/guitools.py b/pyyeti/guitools.py index c391897..39cc42d 100644 --- a/pyyeti/guitools.py +++ b/pyyeti/guitools.py @@ -9,10 +9,16 @@ import os import sys from functools import wraps -import tkinter as tk -from tkinter import filedialog -import tkinter.font as tkFont -from tkinter import ttk + +try: + import tkinter as tk + from tkinter import filedialog + import tkinter.font as tkFont + from tkinter import ttk +except ImportError: + HAVE_TKINTER = False +else: + HAVE_TKINTER = True LASTOPENDIR = None @@ -106,6 +112,9 @@ def askopenfilename(title=None, filetypes=None, initialdir=None): # pragma: no filename = guitools.askopenfilename(filetypes=filetypes) """ + if not HAVE_TKINTER: + msg = "tkinter not available, cannot create GUI dialog" + raise ImportError(msg) global LASTOPENDIR root = tk.Tk() root.withdraw() @@ -158,6 +167,9 @@ def asksaveasfilename(title=None, filetypes=None, initialdir=None): # pragma: n filename = guitools.asksaveasfilename(filetypes=filetypes) """ + if not HAVE_TKINTER: + msg = "tkinter not available, cannot create GUI dialog" + raise ImportError(msg) global LASTSAVEDIR root = tk.Tk() root.withdraw() @@ -357,6 +369,10 @@ def __init__( topstring : string; optional String to print above table """ + if not HAVE_TKINTER: + msg = "tkinter not available, cannot create GUI dialog" + raise ImportError(msg) + self.root = tk.Tk() self.root.title(title) self.tree = None From e481783d5c35d26fbf8c437d53306b6e1af4e02c Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Thu, 12 Mar 2026 10:50:13 -0700 Subject: [PATCH 02/12] Fixed some test errors and warnings. --- pyyeti/nastran/n2p.py | 2 +- pyyeti/tests/test_cb.py | 4 ++-- pyyeti/tests/test_n2p.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyyeti/nastran/n2p.py b/pyyeti/nastran/n2p.py index 8517b2a..0c181d5 100644 --- a/pyyeti/nastran/n2p.py +++ b/pyyeti/nastran/n2p.py @@ -460,7 +460,7 @@ def replace_basic_cs(uset, new_cs_id, new_cs_in_basic=None): # extract the GRID part of the uset table to a numpy array: grids = uset_new.index.get_level_values("dof") > 0 - xyz = uset_new.loc[grids, "x":"z"].values # .copy() + xyz = uset_new.loc[grids, "x":"z"].values.copy() nrows = xyz.shape[0] current_cs_ids = xyz[1::6, 0].astype(int) diff --git a/pyyeti/tests/test_cb.py b/pyyeti/tests/test_cb.py index 0c1575a..910e9a2 100644 --- a/pyyeti/tests/test_cb.py +++ b/pyyeti/tests/test_cb.py @@ -694,7 +694,7 @@ def test_cbcoordchk(): assert abs(chk0.maxerr).max() < 1e-5 # a case where the refpoint_chk should be 'fail': - with pytest.warns(la.LinAlgWarning, match=r"Ill\-conditioned matrix"): + with pytest.warns(la.LinAlgWarning, match=r"ill\-conditioned matrix"): chk2 = cb.cbcoordchk(kaa, b, [25, 26, 27, 31, 32, 33], verbose=False) assert chk2.refpoint_chk == "fail" @@ -1045,7 +1045,7 @@ def test_cbcoordchk3(): assert abs(chk0.maxerr).max() < 1e-5 # a case where the refpoint_chk should be 'fail': - with pytest.warns(la.LinAlgWarning, match=r"Ill\-conditioned matrix"): + with pytest.warns(la.LinAlgWarning, match=r"ill\-conditioned matrix"): chk2 = cb.cbcoordchk(kaa, b, [25, 26, 27, 31, 32, 33], verbose=False) assert chk2.refpoint_chk == "fail" diff --git a/pyyeti/tests/test_n2p.py b/pyyeti/tests/test_n2p.py index 3345e19..f798b7b 100644 --- a/pyyeti/tests/test_n2p.py +++ b/pyyeti/tests/test_n2p.py @@ -1948,7 +1948,8 @@ def test_badrbe3_warn(): uset = n2p.addgrid(None, np.arange(1, n + 1), "b", 0, np.column_stack((x, y, z)), 0) uset = n2p.addgrid(uset, 100, "b", 0, [5, 5, 5], 0) with pytest.warns(RuntimeWarning, match="matrix is poorly conditioned"): - rbe3 = n2p.formrbe3(uset, 100, 123456, [123, [1, 2, 3, 4, 5]]) + with pytest.warns(la.LinAlgWarning, match=r"ill\-conditioned matrix"): + rbe3 = n2p.formrbe3(uset, 100, 123456, [123, [1, 2, 3, 4, 5]]) def test_rbe3_badum(): From 18d03e93b6f6ca31becc17a65c243580447a5dfc Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Thu, 12 Mar 2026 10:50:37 -0700 Subject: [PATCH 03/12] Run tests on this temporary branch. --- .github/workflows/unit-tests.yml | 1 + .github/workflows/wheels.yml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 0954a68..6889340 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - tkinter_optional paths-ignore: - "docs/**" - "MANIFEST.in" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2495621..6fec405 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - tkinter_optional paths-ignore: - "docs/**" - "MANIFEST.in" @@ -79,4 +80,4 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python -m pip install --upgrade packaging twine - python -m twine upload --verbose dist/* + # python -m twine upload --verbose dist/* From 4b9c4865cd6d47a4a0369206fff2f0e9f711d880 Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Thu, 12 Mar 2026 11:07:16 -0700 Subject: [PATCH 04/12] Fixed some test failures on Python 3.10. --- pyyeti/tests/test_cb.py | 7 +++++-- pyyeti/tests/test_n2p.py | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyyeti/tests/test_cb.py b/pyyeti/tests/test_cb.py index 910e9a2..c52f143 100644 --- a/pyyeti/tests/test_cb.py +++ b/pyyeti/tests/test_cb.py @@ -4,6 +4,7 @@ from io import StringIO import tempfile import os +import re import inspect from pyyeti import cb, ytools, nastran from pyyeti.nastran import op2, n2p, op4 @@ -694,7 +695,8 @@ def test_cbcoordchk(): assert abs(chk0.maxerr).max() < 1e-5 # a case where the refpoint_chk should be 'fail': - with pytest.warns(la.LinAlgWarning, match=r"ill\-conditioned matrix"): + regex = re.compile(r"ill\-conditioned matrix", re.IGNORECASE) + with pytest.warns(la.LinAlgWarning, match=regex): chk2 = cb.cbcoordchk(kaa, b, [25, 26, 27, 31, 32, 33], verbose=False) assert chk2.refpoint_chk == "fail" @@ -1045,7 +1047,8 @@ def test_cbcoordchk3(): assert abs(chk0.maxerr).max() < 1e-5 # a case where the refpoint_chk should be 'fail': - with pytest.warns(la.LinAlgWarning, match=r"ill\-conditioned matrix"): + regex = re.compile(r"ill\-conditioned matrix", re.IGNORECASE) + with pytest.warns(la.LinAlgWarning, match=regex): chk2 = cb.cbcoordchk(kaa, b, [25, 26, 27, 31, 32, 33], verbose=False) assert chk2.refpoint_chk == "fail" diff --git a/pyyeti/tests/test_n2p.py b/pyyeti/tests/test_n2p.py index f798b7b..3925f1b 100644 --- a/pyyeti/tests/test_n2p.py +++ b/pyyeti/tests/test_n2p.py @@ -5,6 +5,7 @@ from scipy.io import matlab import io import os +import re from pyyeti import nastran, cb from pyyeti.nastran import n2p, op2, op4 import pytest @@ -1948,7 +1949,8 @@ def test_badrbe3_warn(): uset = n2p.addgrid(None, np.arange(1, n + 1), "b", 0, np.column_stack((x, y, z)), 0) uset = n2p.addgrid(uset, 100, "b", 0, [5, 5, 5], 0) with pytest.warns(RuntimeWarning, match="matrix is poorly conditioned"): - with pytest.warns(la.LinAlgWarning, match=r"ill\-conditioned matrix"): + regex = re.compile(r"ill\-conditioned matrix", re.IGNORECASE) + with pytest.warns(la.LinAlgWarning, match=regex): rbe3 = n2p.formrbe3(uset, 100, 123456, [123, [1, 2, 3, 4, 5]]) From 2739a0662170c329faab9eb6dd34ae351094a1b4 Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Thu, 12 Mar 2026 11:09:44 -0700 Subject: [PATCH 05/12] Updated Github Actions versions. Added tests of Python 3.14. --- .github/workflows/unit-tests.yml | 5 +++-- .github/workflows/wheels.yml | 16 ++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 6889340..62f6e2a 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -28,6 +28,7 @@ jobs: python-version: - "3.12" - "3.13" + - "3.14" extra-doctest-ignore: - "" # The OrderedDict __str__ method changed in Python 3.12 so doctests in the ytools.py module will fail @@ -48,9 +49,9 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Python dependencies diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6fec405..50c3462 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -33,15 +33,15 @@ jobs: - windows-2022 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.13" - name: Build wheels - uses: pypa/cibuildwheel@v2.23.3 + uses: pypa/cibuildwheel@v3.4.0 - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: wheelhouse/*.whl @@ -52,9 +52,9 @@ jobs: needs: [build_wheels] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install dependencies and build sdist @@ -65,13 +65,13 @@ jobs: $PYTHON_VENV/bin/pip install -v build $PYTHON_VENV/bin/python -m build -s . - name: Download wheels - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: cibw-wheels-* merge-multiple: true path: dist/ - name: Upload wheels and sdist - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheels-and-sdist path: dist/ From 63ed121aa6d6667e6c960c326651761c31a209d2 Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Thu, 12 Mar 2026 13:28:28 -0700 Subject: [PATCH 06/12] Removed flag to skip PyPy builds. They are no longer enabled by default (as of cibuildwheel 3.0) so they no longer need to be skipped. --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3b7e10c..88f0e8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,6 @@ line-length = 88 build-frontend = "build" archs = ["auto64"] skip = [ - "pp*", # PyPy "*musl*", # musllinux ] From b3a79845d24985994caf2674d36a64d3273dd0bb Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Thu, 12 Mar 2026 13:37:07 -0700 Subject: [PATCH 07/12] Test the limited API. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 7c5dcb1..6e4800a 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,8 @@ sources=["pyyeti/rainflow/c_rain.c"], include_dirs=[np.get_include()], optional=True, + define_macros=[("Py_LIMITED_API", "0x03100000")], + py_limited_api=True, ), ], ) From 95b0d2d19e1de5d843f01f7ab3c3cd89fff49b20 Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Thu, 12 Mar 2026 13:45:04 -0700 Subject: [PATCH 08/12] Removed use of limited API. --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 6e4800a..7c5dcb1 100644 --- a/setup.py +++ b/setup.py @@ -10,8 +10,6 @@ sources=["pyyeti/rainflow/c_rain.c"], include_dirs=[np.get_include()], optional=True, - define_macros=[("Py_LIMITED_API", "0x03100000")], - py_limited_api=True, ), ], ) From a1f052461b74c0cfe1d499342825b78db40c1748 Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Wed, 18 Mar 2026 17:41:54 -0700 Subject: [PATCH 09/12] Added seeds to make tests deterministic. --- pyyeti/tests/test_fdepsd.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyyeti/tests/test_fdepsd.py b/pyyeti/tests/test_fdepsd.py index ef991b8..b67860d 100644 --- a/pyyeti/tests/test_fdepsd.py +++ b/pyyeti/tests/test_fdepsd.py @@ -23,7 +23,7 @@ def compare(fde1, fde2): def test_fdepsd_absacce(): - np.random.seed(1) + rng = np.random.default_rng(1) TF = 60 # make a 60 second signal sp = 1.0 spec = np.array([[20, sp], [50, sp]]) @@ -35,6 +35,7 @@ def test_fdepsd_absacce(): df=1 / TF, winends=dict(portion=10), gettime=True, + rng=rng, ) freq = np.arange(30.0, 50.1) q = 25 @@ -169,9 +170,9 @@ def test_fdepsd_error(): def test_ski_slope(): - # np.random.seed(1) + rng = np.random.default_rng(1) spec = np.array([[20.0, 1.0], [100.0, 1.0], [150.0, 10.0], [1000.0, 10.0]]) - sig, sr = psd.psd2time(spec, 20, 1000) + sig, sr = psd.psd2time(spec, 20, 1000, rng=rng) sig[0] = sig.max() From 7c74aba41bcf72ebc79e57206442d82d601486f4 Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Wed, 18 Mar 2026 18:06:27 -0700 Subject: [PATCH 10/12] Don't build freethreading wheels - it's unclear if c_rain supports it. --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 88f0e8d..2be4c4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,8 @@ line-length = 88 build-frontend = "build" archs = ["auto64"] skip = [ - "*musl*", # musllinux + "cp3??t-*", # Free-threading on all platforms + "*musl*", # musllinux ] From 304e28810707aafcab309b4990593d84164ba5a7 Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Wed, 18 Mar 2026 18:18:18 -0700 Subject: [PATCH 11/12] Don't run tests on temporary branch. --- .github/workflows/unit-tests.yml | 1 - .github/workflows/wheels.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 62f6e2a..7929eb6 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - tkinter_optional paths-ignore: - "docs/**" - "MANIFEST.in" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 50c3462..db7c272 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - tkinter_optional paths-ignore: - "docs/**" - "MANIFEST.in" From b05d6b9c9cc6d71c8bd6e4e4f65cd2419e81c4aa Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Thu, 19 Mar 2026 08:58:06 -0700 Subject: [PATCH 12/12] Added tests of guitools when tkinter is not available. --- pyyeti/tests/test_guitools.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 pyyeti/tests/test_guitools.py diff --git a/pyyeti/tests/test_guitools.py b/pyyeti/tests/test_guitools.py new file mode 100644 index 0000000..a17b76b --- /dev/null +++ b/pyyeti/tests/test_guitools.py @@ -0,0 +1,31 @@ +import pytest +from pyyeti import guitools + + +@pytest.mark.parametrize("read", [True, False]) +def test_get_file_name_no_tkinter(read, monkeypatch): + monkeypatch.setattr(guitools, "HAVE_TKINTER", False) + with pytest.raises(ImportError, match="tkinter not available.*cannot create GUI"): + guitools.get_file_name(None, read) + + +def test_askopenfilename_no_tkinter(monkeypatch): + monkeypatch.setattr(guitools, "HAVE_TKINTER", False) + with pytest.raises(ImportError, match="tkinter not available.*cannot create GUI"): + guitools.askopenfilename() + + +def test_asksaveasfilename_no_tkinter(monkeypatch): + monkeypatch.setattr(guitools, "HAVE_TKINTER", False) + with pytest.raises(ImportError, match="tkinter not available.*cannot create GUI"): + guitools.asksaveasfilename() + + +def test_multicolumnlistbox_no_tkinter(monkeypatch): + monkeypatch.setattr(guitools, "HAVE_TKINTER", False) + headers = ["First", "Middle", "Last"] + lst1 = ["Tony", "Jennifer", "Albert", "Marion"] + lst2 = ["J.", "M.", "E.", "K."] + lst3 = ["Anderson", "Smith", "Kingsley", "Cotter"] + with pytest.raises(ImportError, match="tkinter not available.*cannot create GUI"): + guitools.MultiColumnListbox("Select person", headers, [lst1, lst2, lst3])