From 488058433b6fe572b243f08d1eee021650ab3575 Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:26:41 +0100 Subject: [PATCH 01/13] improve formatting --- .clang-format | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.clang-format b/.clang-format index 7d6c2803..12ab111c 100644 --- a/.clang-format +++ b/.clang-format @@ -79,6 +79,9 @@ IncludeCategories: - Regex: '^["<]libstored/' Priority: 1 SortPriority: 0 + - Regex: '^<(cassert|assert\.h)>$' + Priority: 3 + SortPriority: 0 - Regex: '.*' Priority: 2 SortPriority: 0 From e0123df3297c4440987c5c0d72ffeba3454ed6d2 Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:31:49 +0100 Subject: [PATCH 02/13] fix consistency --- src/protocol.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/protocol.cpp b/src/protocol.cpp index 7c84e982..6d68fb34 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -1394,7 +1394,7 @@ void Crc8Layer::decode(void* buffer, size_t len) uint8_t* buffer_ = static_cast(buffer); uint8_t crc = init; for(size_t i = 0; i < len - 1; i++) - crc = compute(crc, buffer_[i]); + crc = compute(buffer_[i], crc); if(crc != buffer_[len - 1]) // Invalid. @@ -1407,7 +1407,7 @@ void Crc8Layer::encode(void const* buffer, size_t len, bool last) { uint8_t const* buffer_ = static_cast(buffer); for(size_t i = 0; i < len; i++) - m_crc = compute(m_crc, buffer_[i]); + m_crc = compute(buffer_[i], m_crc); base::encode(buffer, len, false); From 1e0fc2a72668cce6571d2228c4bd4932166f6268 Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:32:04 +0100 Subject: [PATCH 03/13] doc --- src/protocol.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocol.cpp b/src/protocol.cpp index 6d68fb34..948f5d2b 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020-2025 Jochem Rutgers +// SPDX-FileCopyrightText: 2020-2026 Jochem Rutgers // // SPDX-License-Identifier: MPL-2.0 From 693eec8fe0890e3edbbd96b785d1e943aa6d1f05 Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:06:32 +0100 Subject: [PATCH 04/13] fix coro worker scheduling --- CHANGELOG.rst | 17 ++- python/libstored/asyncio/worker.py | 45 ++++---- tests/CMakeLists.txt | 10 ++ tests/test_async.py | 159 +++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 23 deletions(-) create mode 100755 tests/test_async.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 50a2d0ed..e6c6e061 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@  .. - SPDX-FileCopyrightText: 2020-2025 Jochem Rutgers + SPDX-FileCopyrightText: 2020-2026 Jochem Rutgers SPDX-License-Identifier: CC0-1.0 @@ -24,14 +24,27 @@ The format is based on `Keep a Changelog`_, and this project adheres to Added ````` +... + +.. _Unreleased: https://github.com/DEMCON/libstored/compare/v2.2.0...HEAD + + + +`2.2.0`_ - 2026-03-28 +--------------------- + +Added +````` + - ``libstored.protocol.ArqLayer`` for general-purpose ARQ. Fixed ````` - ``ZmqClient`` assertion during cleanup +- AsyncWorker coro scheduling from MainThread asyncio loop. -.. _Unreleased: https://github.com/DEMCON/libstored/compare/v2.1.0...HEAD +.. _2.2.0: https://github.com/DEMCON/libstored/releases/tag/v2.2.0 diff --git a/python/libstored/asyncio/worker.py b/python/libstored/asyncio/worker.py index 4d94e2d0..b030166b 100644 --- a/python/libstored/asyncio/worker.py +++ b/python/libstored/asyncio/worker.py @@ -335,7 +335,29 @@ def run_sync(*args, block: bool = True, sync: bool | None = None, **kwargs) -> t sync is None or (loop is None) == sync or not block ), "sync argument contradicts current context" - if loop is not None and not sync: + w = None + logger = None + if isinstance(self, Work): + logger = self.logger + w = self.worker + if loop is not w.loop: + logger.debug("Running %s in worker %s", f.__qualname__, str(w)) + else: + logger.debug("Running %s in current worker", f.__qualname__) + else: + if hasattr(self, "logger"): + logger = self.logger + logger.debug("Running %s in default worker", f.__qualname__) + global default_worker + w = default_worker + + if w is None or not w.is_running(): + if hasattr(self, "logger"): + logger = self.logger + logger.debug("No worker running, creating new one") + w = AsyncioWorker() + + if loop is not None and not sync and loop is w.loop: # We are in an event loop, just start the coro. coro = f(*args, **kwargs) if block: @@ -343,26 +365,7 @@ def run_sync(*args, block: bool = True, sync: bool | None = None, **kwargs) -> t else: return asyncio.ensure_future(coro) else: - # We are not in an event loop, run the coro in the (default) worker. - w = None - logger = None - if isinstance(self, Work): - logger = self.logger - logger.debug("Running %s in worker %s", f.__qualname__, str(self.worker)) - w = self.worker - else: - if hasattr(self, "logger"): - logger = self.logger - logger.debug("Running %s in default worker", f.__qualname__) - global default_worker - w = default_worker - - if w is None or not w.is_running(): - if hasattr(self, "logger"): - logger = self.logger - logger.debug("No worker running, creating new one") - w = AsyncioWorker() - + # We are not in the (proper) event loop, run the coro in the (default) worker. future = w.execute(f(*args, **kwargs)) if block: return lexc.DeadlockChecker(future).result() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2be6a444..ef95097c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -292,6 +292,16 @@ if(LIBSTORED_HAVE_LIBZMQ ) endif() +if(LIBSTORED_PYLIBSTORED) + add_test(NAME test_async + COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}/../python + ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_async.py + ) + + set_tests_properties(test_async PROPERTIES TIMEOUT 60) + add_launch_json_py(test_async ${CMAKE_CURRENT_SOURCE_DIR}/test_async.py) +endif() + if(VIVADO_CMD) if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/test_fpga/vivado/test_fpga/test_fpga.xpr) execute_process( diff --git a/tests/test_async.py b/tests/test_async.py new file mode 100755 index 00000000..273ed571 --- /dev/null +++ b/tests/test_async.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: 2020-2026 Jochem Rutgers +# +# SPDX-License-Identifier: MPL-2.0 + +import asyncio +import logging +import os +import sys +import threading +import unittest + +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../python")) +) + +import libstored.asyncio.worker + +logger = logging.getLogger(__name__) + + +def f() -> tuple[threading.Thread, asyncio.AbstractEventLoop | None]: + t = threading.current_thread() + loop = None + try: + loop = asyncio.get_running_loop() + except RuntimeError: + pass + + logger.info( + "running in thread %s, loop %s", + t.name, + hex(id(loop)) if loop is not None else None, + ) + + return (t, loop) + + +async def coro_f() -> tuple[threading.Thread, asyncio.AbstractEventLoop | None]: + return f() + + +@libstored.asyncio.worker.run_sync +async def async_f() -> tuple[threading.Thread, asyncio.AbstractEventLoop | None]: + return await coro_f() + + +class WorkF(libstored.asyncio.worker.Work): + @libstored.asyncio.worker.Work.thread_safe + def f(self) -> tuple[threading.Thread, asyncio.AbstractEventLoop | None]: + return f() + + @libstored.asyncio.worker.Work.thread_safe + def f2(self) -> tuple[threading.Thread, asyncio.AbstractEventLoop | None]: + return self.f() + + +class AsyncTest(unittest.TestCase): + def test_normal(self): + # Just runs here. + context = f() + self.assertEqual(context, (threading.current_thread(), None)) + + def test_coro(self): + # Runs in the created loop in the current thread. + context = asyncio.run(coro_f()) + self.assertEqual(context[0], threading.current_thread()) + self.assertIsNotNone(context[1]) + + def test_sync(self): + # Runs in the default worker thread and loop. + context = async_f() + self.assertIsNotNone(libstored.asyncio.worker.default_worker) + assert libstored.asyncio.worker.default_worker + self.assertEqual( + context, + ( + libstored.asyncio.worker.default_worker.thread, + libstored.asyncio.worker.default_worker.loop, + ), + ) + + def test_sync_nonblock(self): + # Runs in the default worker thread and loop. + context = async_f(block=False).result() + self.assertIsNotNone(libstored.asyncio.worker.default_worker) + assert libstored.asyncio.worker.default_worker + self.assertEqual( + context, + ( + libstored.asyncio.worker.default_worker.thread, + libstored.asyncio.worker.default_worker.loop, + ), + ) + + def test_sync_in_loop(self): + # Still runs in the default worker thread and loop and not in the same loop. + async def test(): + context = await async_f() + self.assertIsNotNone(libstored.asyncio.worker.default_worker) + assert libstored.asyncio.worker.default_worker + self.assertEqual( + context, + ( + libstored.asyncio.worker.default_worker.thread, + libstored.asyncio.worker.default_worker.loop, + ), + ) + + asyncio.run(test()) + + def test_thread_safe(self): + # Runs in the default worker thread and loop. + context = WorkF().f() + self.assertIsNotNone(libstored.asyncio.worker.default_worker) + assert libstored.asyncio.worker.default_worker + self.assertEqual( + context, + ( + libstored.asyncio.worker.default_worker.thread, + libstored.asyncio.worker.default_worker.loop, + ), + ) + + def test_thread_safe_nonblock(self): + # Runs in the default worker thread and loop. + context = WorkF().f(block=False).result() + self.assertIsNotNone(libstored.asyncio.worker.default_worker) + assert libstored.asyncio.worker.default_worker + self.assertEqual( + context, + ( + libstored.asyncio.worker.default_worker.thread, + libstored.asyncio.worker.default_worker.loop, + ), + ) + + def test_thread_safe2(self): + # Runs in the default worker thread and loop. + context = WorkF().f2() + self.assertIsNotNone(libstored.asyncio.worker.default_worker) + assert libstored.asyncio.worker.default_worker + self.assertEqual( + context, + ( + libstored.asyncio.worker.default_worker.thread, + libstored.asyncio.worker.default_worker.loop, + ), + ) + + +def main(): + logging.basicConfig(level=logging.DEBUG) + unittest.main() + + +if __name__ == "__main__": + main() From 659ed7b07805360697006fff88671abae8e17fda Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:35:41 +0100 Subject: [PATCH 05/13] fix stopping fast poll --- CHANGELOG.rst | 3 ++- python/libstored/asyncio/zmq.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e6c6e061..17a206ae 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -42,7 +42,8 @@ Fixed ````` - ``ZmqClient`` assertion during cleanup -- AsyncWorker coro scheduling from MainThread asyncio loop. +- ``AsyncioWorker`` coro scheduling from MainThread asyncio loop. +- Fix stopping fast poll in ``ZmqClient`` when all objects are stopped polling. .. _2.2.0: https://github.com/DEMCON/libstored/releases/tag/v2.2.0 diff --git a/python/libstored/asyncio/zmq.py b/python/libstored/asyncio/zmq.py index e046c4d0..cd652ef5 100644 --- a/python/libstored/asyncio/zmq.py +++ b/python/libstored/asyncio/zmq.py @@ -3045,7 +3045,7 @@ async def _poll(self, o: Object, interval_s: float | None): await self._fast_poll_macro.remove(o) await self.alias(o, temporary=True, permanentRef=self._fast_poll_macro) - if len(self._fast_poll_macro) == 0: + if len(self._fast_poll_macro) <= 1: # time is always included, so check for <= 1 await self._poll_fast_stop() # Stop trace, if any From 0c0fe4a5b3e83ed5ededd33fdb344fa5e595f9ca Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:07:15 +0100 Subject: [PATCH 06/13] doc --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 17a206ae..0c6cdeb5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -44,6 +44,7 @@ Fixed - ``ZmqClient`` assertion during cleanup - ``AsyncioWorker`` coro scheduling from MainThread asyncio loop. - Fix stopping fast poll in ``ZmqClient`` when all objects are stopped polling. +- ``libstored.gui`` performance on Windows. .. _2.2.0: https://github.com/DEMCON/libstored/releases/tag/v2.2.0 From 837f55bbcaf53a4d32fddd6e957c14a1d03f0b6e Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:24:34 +0100 Subject: [PATCH 07/13] fixing CI --- .github/workflows/ci.yml | 18 +++++++++--------- dist/common/requirements.txt | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c2e8138..93012db2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: CXX: g++-${{matrix.gcc}} steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: bootstrap run: | sudo apt update @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: bootstrap run: | sudo apt update @@ -86,10 +86,10 @@ jobs: # zth: zth steps: - name: checkout - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: '3.12' + python-version: '3.13' - name: bootstrap run: | dist\win32\bootstrap.cmd @@ -104,7 +104,7 @@ jobs: runs-on: macos-latest steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: bootstrap run: dist/macos/bootstrap.sh - name: venv @@ -120,7 +120,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: bootstrap run: | sudo apt update @@ -137,7 +137,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: bootstrap run: | sudo apt update @@ -153,7 +153,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: bootstrap run: | sudo apt update diff --git a/dist/common/requirements.txt b/dist/common/requirements.txt index a4a57c7b..62138d0d 100644 --- a/dist/common/requirements.txt +++ b/dist/common/requirements.txt @@ -17,5 +17,5 @@ breathe sphinxcontrib-wavedrom # Other -reuse +reuse >= 6.0.0 cmake_format From b2c08e025eed8f7da176c7c93af134fe5fca2e07 Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:41:10 +0100 Subject: [PATCH 08/13] migrate --- .reuse/dep5 | 13 ------------- REUSE.toml | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 13 deletions(-) delete mode 100644 .reuse/dep5 create mode 100644 REUSE.toml diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index b109c00f..00000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,13 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ - -Files: extern/material.io/* -Copyright: Google -License: Apache-2.0 - -Files: presentation/* -Copyright: 2020-2023 Jochem Rutgers -License: CC-BY-4.0 - -Files: .vscode/* -Copyright: 2020-2024 Jochem Rutgers -License: CC0-1.0 diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 00000000..43e45c05 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2020-2026 Jochem Rutgers +# +# SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = "extern/material.io/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "Google" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "presentation/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2020-2026 Jochem Rutgers" +SPDX-License-Identifier = "CC-BY-4.0" + +[[annotations]] +path = ".vscode/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2020-2026 Jochem Rutgers" +SPDX-License-Identifier = "CC0-1.0" From 99c45c4bd5b450246cb2b0f9a9cf111d09a750bf Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:41:25 +0100 Subject: [PATCH 09/13] add versions --- .github/workflows/ci.yml | 6 +++--- dist/common/build.sh | 8 +++++--- dist/win32/build.cmd | 10 +++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93012db2..9a020773 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020-2025 Jochem Rutgers +# SPDX-FileCopyrightText: 2020-2026 Jochem Rutgers # # SPDX-License-Identifier: CC0-1.0 @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: gcc: [12, 13, 14] - cxx: [C++03, C++11, C++14, C++17] + cxx: [C++03, C++11, C++14, C++17, C++20, C++23] zmq: [zmq, nozmq] include: - gcc: 13 @@ -68,7 +68,7 @@ jobs: fail-fast: false matrix: compiler: [msvc, gcc] - cxx: [C++03, C++11, C++14, C++17] + cxx: [C++03, C++11, C++14, C++17, C++20, C++23] zmq: [zmq, nozmq] exclude: - compiler: msvc diff --git a/dist/common/build.sh b/dist/common/build.sh index 5e518fe4..dfe2ecd2 100644 --- a/dist/common/build.sh +++ b/dist/common/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -# SPDX-FileCopyrightText: 2020-2025 Jochem Rutgers +# SPDX-FileCopyrightText: 2020-2026 Jochem Rutgers # # SPDX-License-Identifier: MPL-2.0 @@ -18,9 +18,9 @@ trap gotErr ERR function show_help { echo -e "Usage: $0 [...] [--] []\n" echo "where opt is:" - echo " Debug RelWithDebInfo Release" + echo " Debug RelWithDebInfo Release" echo " Set CMAKE_BUILD_TYPE to this value" - echo " C++98 C++03 C++11 C++14 C++17 C++20" + echo " C++98 C++03 C++11 C++14 C++17 C++20 C++23" echo " Set the C++ standard" echo " conf Configure only, don't build" echo " dev Enable development-related options" @@ -77,6 +77,8 @@ while [[ ! -z ${1:-} ]]; do cmake_opts="${cmake_opts} -DCMAKE_CXX_STANDARD=17 -DCMAKE_C_STANDARD=11";; C++20) cmake_opts="${cmake_opts} -DCMAKE_CXX_STANDARD=20 -DCMAKE_C_STANDARD=11";; + C++23) + cmake_opts="${cmake_opts} -DCMAKE_CXX_STANDARD=23 -DCMAKE_C_STANDARD=11";; conf) do_build=0;; dev) diff --git a/dist/win32/build.cmd b/dist/win32/build.cmd index db544b3a..63e0a2b4 100644 --- a/dist/win32/build.cmd +++ b/dist/win32/build.cmd @@ -1,6 +1,6 @@ @echo off -rem SPDX-FileCopyrightText: 2020-2025 Jochem Rutgers +rem SPDX-FileCopyrightText: 2020-2026 Jochem Rutgers rem rem SPDX-License-Identifier: MPL-2.0 @@ -85,6 +85,10 @@ if %1 == C++20 ( set cmake_opts=%cmake_opts% -DCMAKE_CXX_STANDARD=20 -DCMAKE_C_STANDARD=11 goto next_param ) +if %1 == C++23 ( + set cmake_opts=%cmake_opts% -DCMAKE_CXX_STANDARD=23 -DCMAKE_C_STANDARD=11 + goto next_param +) if %1 == conf ( set do_build=0 goto next_param @@ -229,10 +233,10 @@ exit /b 0 echo Usage: %0 [^...] [--] [^] echo. echo where opt is: -echo Debug RelWithDebInfo Release +echo Debug RelWithDebInfo Release echo Set CMAKE_BUILD_TYPE to this value echo gcc Use gcc instead of default compiler -echo C++98 C++03 C++11 C++14 C++17 C++20 +echo C++98 C++03 C++11 C++14 C++17 C++20 C++23 echo Set the C++ standard echo conf Configure only, don't build echo dev Enable development-related options From 2d715bd2c60cdedd60e05b2b903316883ede8159 Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:41:31 +0100 Subject: [PATCH 10/13] prepare release --- version/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version/version.txt b/version/version.txt index a74d18b8..ccbccc3d 100644 --- a/version/version.txt +++ b/version/version.txt @@ -1 +1 @@ -2.2.0-alpha +2.2.0 From edf64a388864aee87773daac70b7c2ab100d1dad Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:26:28 +0100 Subject: [PATCH 11/13] cleanup --- python/libstored/gui/__main__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/libstored/gui/__main__.py b/python/libstored/gui/__main__.py index f859fcda..3f0379f0 100644 --- a/python/libstored/gui/__main__.py +++ b/python/libstored/gui/__main__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020-2025 Jochem Rutgers +# SPDX-FileCopyrightText: 2020-2026 Jochem Rutgers # # SPDX-License-Identifier: MPL-2.0 @@ -30,7 +30,6 @@ from .. import exceptions as lexc from .. import protocol as lprot - ##################################################################### # Style # @@ -694,6 +693,7 @@ def set_objects(self, objects: list[laio_zmq.Object]): for o in self._objects: o.destroy() self._objects = [] + self._filtered_objects = [] for o in natsort.natsorted(objects, key=lambda o: o.name, alg=natsort.ns.IGNORECASE): object_row = ObjectRow(self._app, self, o, show_plot=self._show_plot) @@ -1337,6 +1337,8 @@ def cleanup(self): plotter = None self.disconnect_all() + self._objects.set_objects([]) + self._polled_objects.set_objects([]) self.logger.debug("Close client") self._close_async() super().cleanup() From f92c59a58ae3be3877e48c68f9dfcc09138cdb49 Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:35:44 +0100 Subject: [PATCH 12/13] tweak cleanup --- python/libstored/gui/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/libstored/gui/__main__.py b/python/libstored/gui/__main__.py index 3f0379f0..1075abbe 100644 --- a/python/libstored/gui/__main__.py +++ b/python/libstored/gui/__main__.py @@ -1193,8 +1193,8 @@ def __init__( self.root.title(f"libstored GUI") icon_path = os.path.join(os.path.dirname(__file__), "twotone_bug_report_black_48dp.png") - icon = tk.PhotoImage(file=icon_path) - self.root.iconphoto(False, icon) + self._icon = tk.PhotoImage(file=icon_path) + self.root.iconphoto(False, self._icon) global plotter if Plotter.available and plotter is None: From 74d6560b2dc3f6afb4f09fa4679c17eb529bcfd3 Mon Sep 17 00:00:00 2001 From: Jochem Rutgers <68805714+jhrutgers@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:45:14 +0100 Subject: [PATCH 13/13] doc --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c6cdeb5..05b8ac57 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -30,7 +30,7 @@ Added -`2.2.0`_ - 2026-03-28 +`2.2.0`_ - 2026-03-27 --------------------- Added