From d32e47b15e7fa14b142f0fd28eb1d41a966580c2 Mon Sep 17 00:00:00 2001 From: Hanspeter Schaub Date: Wed, 8 Apr 2026 06:44:35 -0600 Subject: [PATCH 1/8] [#1354] update swig to latest version Removes some system wide swig warning suppressions --- .github/workflows/requirements_dev.txt | 4 +--- pyproject.toml | 7 +------ requirements_dev.txt | 2 +- setup.py | 2 +- src/CMakeLists.txt | 2 +- src/conftest.py | 7 ------- src/pytest.ini | 5 ----- 7 files changed, 5 insertions(+), 24 deletions(-) diff --git a/.github/workflows/requirements_dev.txt b/.github/workflows/requirements_dev.txt index 0cb58570423..ed7adc2edec 100644 --- a/.github/workflows/requirements_dev.txt +++ b/.github/workflows/requirements_dev.txt @@ -4,9 +4,7 @@ conan packaging setuptools setuptools-scm -# TODO: The SWIG 4.4.X is incompatible with Basilisk. Pin to SWIG 4.3.1 until -# Basilisk is updated to support SWIG 4.4.X -swig<=4.3.1,>=4.2.1 +swig libclang pytest diff --git a/pyproject.toml b/pyproject.toml index b7b0b24b590..ef7b62d5350 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ requires = [ # Requirements for building Basilisk through conanfile "conan>=2.0.5,<=2.23.0", "cmake>=3.26,<4.0", - "swig>=4.2.1,<=4.3.1", # Known to work with https://github.com/nightlark/swig-pypi/pull/120 + "swig>=4.2.1,<5", # Support Basilisk's established SWIG baseline while allowing the latest 4.4.x releases. "libclang>=15.0.6.1,<=18.1.1" # Required by generatePayloadMetaJson.py (message code generation) ] @@ -105,8 +105,3 @@ markers = [ "scenarioTest: mark a test as a tutorial scenario test.", "ciSkip: mark a test that should be skipped on the CI test runs", ] -filterwarnings = [ - "ignore:builtin type SwigPyPacked has no __module__ attribute:DeprecationWarning", - "ignore:builtin type SwigPyObject has no __module__ attribute:DeprecationWarning", - "ignore:builtin type swigvarlink has no __module__ attribute:DeprecationWarning", -] diff --git a/requirements_dev.txt b/requirements_dev.txt index e2a2a870336..8af84b73c89 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,7 +4,7 @@ conan>=2.0.5,<=2.23.0 packaging>=24,<26 setuptools>=70.1.0,<=80.9.0 setuptools-scm>=8.0,<=9.2.2 -swig<=4.3.1,>=4.2.1 +swig>=4.2.1,<5 libclang>=15.0.6.1,<=18.1.1 pytest>=8.3.5,<=9.0.1 diff --git a/setup.py b/setup.py index e38034abcf9..c18164af6b9 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,7 @@ def finalize_options(self) -> None: # Set limited ABI compatibility by default, targeting the minimum required Python version. # See https://docs.python.org/3/c-api/stable.html - # NOTE: Swig 4.2.1 or higher is required, see https://github.com/swig/swig/pull/2727 + # NOTE: Swig 4.2.1 or higher is required. Newer 4.4.x releases avoid deprecated wrapper warnings on newer Python. min_version = next(self.distribution.python_requires.filter([f"3.{i}" for i in range(2, 100)])).replace(".", "") wheel_py_limited = f"cp{min_version}" bdist_wheel = self.reinitialize_command("bdist_wheel", py_limited_api=wheel_py_limited) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cbbc3859208..cc4d0de7600 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -71,7 +71,7 @@ if(PY_LIMITED_API AND NOT PY_LIMITED_API STREQUAL "") # compatibility (basically, we can build a single Python wheel to # support all future Python versions). # See https://docs.python.org/3/c-api/stable.html - # NOTE: Swig 4.2.0 is required, see https://github.com/swig/swig/pull/2727 + # NOTE: Swig 4.2.0 is required. Swig 4.4.x avoids the deprecated wrapper warnings on newer Python releases. add_definitions("-DPy_LIMITED_API=${PY_LIMITED_API}") # Support for current Python version # Force libraries to link to the Stable ABI module instead. diff --git a/src/conftest.py b/src/conftest.py index bddf57f87a5..4b81b77c9e4 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -21,19 +21,12 @@ import shutil import subprocess import sys -import warnings from datetime import date import matplotlib as mpl import matplotlib.pyplot as plt import pytest -warnings.filterwarnings( - "ignore", - message="builtin type swigvarlink has no __module__ attribute", - category=DeprecationWarning, -) - SHOW_PLOTS_REMOVAL_DATE = date(2027, 2, 12) SHOW_PLOTS_DEPRECATION_MESSAGE = ( "The pytest option '--show_plots' is deprecated and will be removed after February 12, 2027." diff --git a/src/pytest.ini b/src/pytest.ini index eaf99cf50fe..5049969648d 100644 --- a/src/pytest.ini +++ b/src/pytest.ini @@ -4,8 +4,3 @@ markers = slowtest: mark a test as a slow unit test. scenarioTest: mark a test as a tutorial scenario test. ciSkip: mark a test that should be skipped on the CI test runs - -filterwarnings = - ignore:builtin type SwigPyPacked has no __module__ attribute:DeprecationWarning - ignore:builtin type SwigPyObject has no __module__ attribute:DeprecationWarning - ignore:builtin type swigvarlink has no __module__ attribute:DeprecationWarning From f48629f0568c15bf68d4414b81d27cfa8d02761f Mon Sep 17 00:00:00 2001 From: Hanspeter Schaub Date: Wed, 8 Apr 2026 06:44:57 -0600 Subject: [PATCH 2/8] [#1354] Use latest SWIG in canary dependency installs Update the canary requirements to install the latest available `swig` package instead of the previously pinned 4.3.1 range. Also extend the shared build action's pip cache dependency paths to include the `.github/workflows` requirement files so canary dependency changes correctly invalidate the pip cache and are reflected in CI. --- .github/actions/build/action.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index d0b47a6f4de..d56d24aa04c 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -29,6 +29,9 @@ runs: requirements_dev.txt requirements_doc.txt requirements.txt + .github/workflows/requirements_dev.txt + .github/workflows/requirements_doc.txt + .github/workflows/requirements.txt - name: Setup system dependencies uses: ./.github/actions/setup From 5a64cf5003dac4ff12d41d99b6eed754f4cea4d2 Mon Sep 17 00:00:00 2001 From: Hanspeter Schaub Date: Sun, 12 Apr 2026 10:11:46 -0600 Subject: [PATCH 3/8] [#1354] remove the swig warning supression We now support swig 4.4.1 which eliminates the need for this. --- .../spaceWeatherData/spaceWeatherData.i | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/simulation/environment/spaceWeatherData/spaceWeatherData.i b/src/simulation/environment/spaceWeatherData/spaceWeatherData.i index b7d1196206c..11b55f2a25d 100644 --- a/src/simulation/environment/spaceWeatherData/spaceWeatherData.i +++ b/src/simulation/environment/spaceWeatherData/spaceWeatherData.i @@ -25,26 +25,6 @@ #include "spaceWeatherData.h" %} -%pythonbegin %{ -import warnings as _warnings - -_warnings.filterwarnings( - "ignore", - message=r"builtin type SwigPyPacked has no __module__ attribute", - category=DeprecationWarning, -) -_warnings.filterwarnings( - "ignore", - message=r"builtin type SwigPyObject has no __module__ attribute", - category=DeprecationWarning, -) -_warnings.filterwarnings( - "ignore", - message=r"builtin type swigvarlink has no __module__ attribute", - category=DeprecationWarning, -) -%} - %pythoncode %{ from Basilisk.architecture.swig_common_model import * %} From e775bab003dc5be63420d5f371dcbcffde23456e Mon Sep 17 00:00:00 2001 From: Hanspeter Schaub Date: Sun, 12 Apr 2026 10:14:21 -0600 Subject: [PATCH 4/8] [#1354] exclude swig 4.4.0 This version had some regressions that caused the BSK build to fail. This is fixed in swig 4.4.1 --- pyproject.toml | 2 +- requirements_dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ef7b62d5350..764b064ef48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ requires = [ # Requirements for building Basilisk through conanfile "conan>=2.0.5,<=2.23.0", "cmake>=3.26,<4.0", - "swig>=4.2.1,<5", # Support Basilisk's established SWIG baseline while allowing the latest 4.4.x releases. + "swig>=4.2.1,!=4.4.0,<5", # Support Basilisk's established SWIG baseline while avoiding the known-bad 4.4.0 release. "libclang>=15.0.6.1,<=18.1.1" # Required by generatePayloadMetaJson.py (message code generation) ] diff --git a/requirements_dev.txt b/requirements_dev.txt index 8af84b73c89..8a9fa3539e2 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,7 +4,7 @@ conan>=2.0.5,<=2.23.0 packaging>=24,<26 setuptools>=70.1.0,<=80.9.0 setuptools-scm>=8.0,<=9.2.2 -swig>=4.2.1,<5 +swig>=4.2.1,!=4.4.0,<5 libclang>=15.0.6.1,<=18.1.1 pytest>=8.3.5,<=9.0.1 From a8d7d24892b09552bb0abe6d80c2a3e66fe460a9 Mon Sep 17 00:00:00 2001 From: Hanspeter Schaub Date: Sun, 12 Apr 2026 10:47:03 -0600 Subject: [PATCH 5/8] [#1354] Close SPICE thread-safety test multiprocessing resources --- .../_UnitTest/test_spiceThreadSafety.py | 139 ++++++++++++------ 1 file changed, 98 insertions(+), 41 deletions(-) diff --git a/src/simulation/environment/spiceInterface/_UnitTest/test_spiceThreadSafety.py b/src/simulation/environment/spiceInterface/_UnitTest/test_spiceThreadSafety.py index e85a2fa621f..d2cb66e272e 100644 --- a/src/simulation/environment/spiceInterface/_UnitTest/test_spiceThreadSafety.py +++ b/src/simulation/environment/spiceInterface/_UnitTest/test_spiceThreadSafety.py @@ -232,6 +232,59 @@ def _runTestWithTimeout(resultQueue, numWorkers, iterationsPerWorker): False, ) ) + finally: + _closeQueue(resultQueue) + + +def _closeQueue(resultQueue): + """ + Close a multiprocessing queue and wait for its feeder thread. + + Parameters + ---------- + resultQueue : multiprocessing.Queue + Queue to close after all expected data has been read or written. + """ + try: + resultQueue.close() + except (OSError, ValueError): + pass + try: + resultQueue.join_thread() + except (AssertionError, OSError, ValueError): + pass + + +def _closeProcess(testProcess): + """ + Release multiprocessing process resources after the process exits. + + Parameters + ---------- + testProcess : multiprocessing.Process + Process object to close after it has been joined. + """ + try: + testProcess.close() + except (OSError, ValueError): + pass + + +def _terminateProcess(testProcess): + """ + Stop a multiprocessing process and wait for shutdown. + + Parameters + ---------- + testProcess : multiprocessing.Process + Process object to terminate. + """ + testProcess.terminate() + shutdownTimeout = 1 # [s] + testProcess.join(shutdownTimeout) + if testProcess.is_alive(): + os.kill(testProcess.pid, 9) + testProcess.join(shutdownTimeout) @pytest.mark.flaky(reruns=3) @@ -260,34 +313,37 @@ def testSpiceThreadSafety(numWorkers, iterationsPerWorker): target=_runTestWithTimeout, args=(resultQueue, numWorkers, iterationsPerWorker), ) - testProcess.start() + try: + testProcess.start() - timeoutSeconds = 60 - testProcess.join(timeoutSeconds) + timeoutSeconds = 60 # [s] + testProcess.join(timeoutSeconds) - if testProcess.is_alive(): - # Hard timeout: kill the worker process and fail the test - testProcess.terminate() - testProcess.join(1) if testProcess.is_alive(): - os.kill(testProcess.pid, 9) - pytest.fail(f"Thread safety test timed out after {timeoutSeconds} seconds") - - try: - results, success = resultQueue.get(block=False) - - if isinstance(results, dict) and "error" in results: + # Hard timeout: kill the worker process and fail the test + _terminateProcess(testProcess) pytest.fail( - "Thread safety test failed with error: " - f"{results['error']}\n{results.get('traceback')}" + f"Thread safety test timed out after {timeoutSeconds} seconds" ) - assert success, "Thread safety test reported thread-safety issues" - assert results["failedIterations"] == 0, ( - "Some iterations failed in the thread-safety test" - ) - except queue.Empty: - pytest.fail("Thread safety test completed but did not return any results") + try: + results, success = resultQueue.get(block=False) + + if isinstance(results, dict) and "error" in results: + pytest.fail( + "Thread safety test failed with error: " + f"{results['error']}\n{results.get('traceback')}" + ) + + assert success, "Thread safety test reported thread-safety issues" + assert results["failedIterations"] == 0, ( + "Some iterations failed in the thread-safety test" + ) + except queue.Empty: + pytest.fail("Thread safety test completed but did not return any results") + finally: + _closeQueue(resultQueue) + _closeProcess(testProcess) if __name__ == "__main__": @@ -307,28 +363,29 @@ def testSpiceThreadSafety(numWorkers, iterationsPerWorker): target=_runTestWithTimeout, args=(resultQueue, numWorkers, iterationsPerWorker), ) - testProcess.start() + try: + testProcess.start() - timeoutSeconds = 60 - testProcess.join(timeoutSeconds) + timeoutSeconds = 60 # [s] + testProcess.join(timeoutSeconds) - if testProcess.is_alive(): - testProcess.terminate() - testProcess.join(1) if testProcess.is_alive(): - os.kill(testProcess.pid, 9) - print(f"ERROR: Thread safety test timed out after {timeoutSeconds} seconds") - sys.exit(2) + _terminateProcess(testProcess) + print(f"ERROR: Thread safety test timed out after {timeoutSeconds} seconds") + sys.exit(2) - try: - results, success = resultQueue.get(block=False) + try: + results, success = resultQueue.get(block=False) - if isinstance(results, dict) and "error" in results: - print(f"ERROR: Thread safety test failed with error: {results['error']}") - print(results.get("traceback")) - sys.exit(1) + if isinstance(results, dict) and "error" in results: + print(f"ERROR: Thread safety test failed with error: {results['error']}") + print(results.get("traceback")) + sys.exit(1) - sys.exit(0 if success else 1) - except queue.Empty: - print("ERROR: Thread safety test completed but did not return results") - sys.exit(1) + sys.exit(0 if success else 1) + except queue.Empty: + print("ERROR: Thread safety test completed but did not return results") + sys.exit(1) + finally: + _closeQueue(resultQueue) + _closeProcess(testProcess) From 3d38070c8bbe784b3af24a8c7f57fc4f8df7f4bf Mon Sep 17 00:00:00 2001 From: Hanspeter Schaub Date: Sun, 12 Apr 2026 10:50:00 -0600 Subject: [PATCH 6/8] [#1354] Close pytest report configuration file handle --- src/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conftest.py b/src/conftest.py index 4b81b77c9e4..b1e609436d0 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -109,4 +109,5 @@ def reset_matplotlib_state(): quit() if 'pytest-html' in installed_packages: - exec(open(path + "/reportconf.py").read(), globals()) + with open(path + "/reportconf.py") as reportConfig: + exec(reportConfig.read(), globals()) From 9381565ee100350f16b0a797775d9063d58b751c Mon Sep 17 00:00:00 2001 From: Hanspeter Schaub Date: Sun, 12 Apr 2026 11:13:43 -0600 Subject: [PATCH 7/8] [#1354] Close pytest rerun socket resources during xdist runs Add local pytest cleanup for ResourceWarning messages emitted when running the test suite with pytest-xdist and pytest-rerunfailures. The rerunfailures plugin creates localhost sockets to coordinate rerun state between the xdist controller and workers, but version 16.1 does not close all of those sockets before Python interpreter shutdown. Update src/conftest.py to close the plugin-owned failures_db socket during pytest_unconfigure(). Also wrap pytest-rerunfailures ServerStatusDB connection handling so accepted server-side sockets are closed when their handler exits. This preserves rerunfailures behavior while preventing unclosed socket warnings during parallel pytest runs. Also keep reportconf.py loading inside a context manager so pytest-html report configuration does not leave an open file handle. Verified with: - pytest -q -n 2 src/architecture/messaging/_UnitTest/test_CMsgTimeWritten.py - pytest -q -n 2 src/simulation/environment/spiceInterface/_UnitTest/test_spiceThreadSafety.py - pytest -n auto from src --- src/conftest.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/conftest.py b/src/conftest.py index b1e609436d0..1674a335908 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -35,6 +35,39 @@ "The pytest option '--show_plots' has been deprecated for a year and will be removed shortly." ) + +def _patch_rerunfailures_socket_cleanup(): + """ + Close pytest-rerunfailures server-side sockets when xdist is active. + + pytest-rerunfailures 16.1 opens localhost sockets to coordinate reruns + between xdist workers, but accepted server connections are not closed by + the plugin. This narrow patch preserves the plugin handler while ensuring + each accepted connection is closed when the handler exits. + """ + try: + import pytest_rerunfailures + except ImportError: + return + + server_status_db = getattr(pytest_rerunfailures, "ServerStatusDB", None) + if server_status_db is None: + return + if getattr(server_status_db, "_bsk_socket_cleanup_patched", False): + return + + original_run_connection = server_status_db.run_connection + + def run_connection_with_socket_close(self, conn): + with conn: + return original_run_connection(self, conn) + + server_status_db.run_connection = run_connection_with_socket_close + server_status_db._bsk_socket_cleanup_patched = True + + +_patch_rerunfailures_socket_cleanup() + filename = inspect.getframeinfo(inspect.currentframe()).filename path = os.path.dirname(os.path.abspath(filename)) print(path) @@ -71,6 +104,17 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): terminalreporter.write_line(message, **{terminal_color: True}, bold=True) +def pytest_unconfigure(config): + failures_db = getattr(config, "failures_db", None) + socket_handle = getattr(failures_db, "sock", None) + if socket_handle is None: + return + try: + socket_handle.close() + except OSError: + pass + + @pytest.fixture(scope="module") def show_plots(request): return request.config.getoption("--show_plots") From bd4a7a5ea5dd59ed4314d33bb57f13263ddcf6fa Mon Sep 17 00:00:00 2001 From: Hanspeter Schaub Date: Sun, 12 Apr 2026 11:22:32 -0600 Subject: [PATCH 8/8] [#1354] add release notes --- docs/source/Support/bskKnownIssues.rst | 10 +++++----- .../1354-swig-pytest-cleanup.rst | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 docs/source/Support/bskReleaseNotesSnippets/1354-swig-pytest-cleanup.rst diff --git a/docs/source/Support/bskKnownIssues.rst b/docs/source/Support/bskKnownIssues.rst index e64a5061049..15f7b2583e8 100644 --- a/docs/source/Support/bskKnownIssues.rst +++ b/docs/source/Support/bskKnownIssues.rst @@ -10,11 +10,11 @@ Basilisk Known Issues Version |release| ----------------- -- When building from source on Python 3.13 using SWIG 4.4.0, a build failure may occur - if ``pyLimitedAPI`` is set to an ABI lower than Python 3.13 (e.g., ``0x03080000``). - SWIG 4.4.0 introduces a new C-API codepath for Python 3.13 that expects newer - definition macros which are not present when targeting older ``abi3`` compatibility. As such, when building - Basilisk with Python 3.13 or above, we automatically default to using the newer cp313 ABI. +- SWIG 4.4.0 caused Basilisk build failures in some Python 3.13+ source-build configurations. + The development dependency range now excludes SWIG 4.4.0, and SWIG 4.4.1 has been verified to build + successfully. If source builds fail with SWIG 4.4.0 or emit ``builtin type swigvarlink has no __module__ attribute`` + warnings, upgrade to SWIG 4.4.1 or newer. + Version 2.10.0 (April 2, 2026) ------------------------------ diff --git a/docs/source/Support/bskReleaseNotesSnippets/1354-swig-pytest-cleanup.rst b/docs/source/Support/bskReleaseNotesSnippets/1354-swig-pytest-cleanup.rst new file mode 100644 index 00000000000..ff5156376d1 --- /dev/null +++ b/docs/source/Support/bskReleaseNotesSnippets/1354-swig-pytest-cleanup.rst @@ -0,0 +1,2 @@ +- Updated SWIG dependency support to allow compatible latest SWIG releases while excluding SWIG 4.4.0 due to compile regressions observed during testing. If you are getting lots of ``builtin type swigvarlink has no __module__ attribute`` warnings upgrade to SWIG 4.4.1 +- Cleaned up pytest resource handling so parallel test runs with pytest-xdist and pytest-rerunfailures no longer report unclosed socket warnings.