From cf518e96b9ed2745485ffc8cdcc8a439a1b9a414 Mon Sep 17 00:00:00 2001 From: songmeo Date: Sat, 21 Mar 2026 20:17:15 +0000 Subject: [PATCH 1/8] Replace deprecated pkg_resources with importlib.resources pkg_resources is no longer bundled by default in newer Python versions, causing ModuleNotFoundError on startup. --- dronecan_gui_tool/widgets/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dronecan_gui_tool/widgets/__init__.py b/dronecan_gui_tool/widgets/__init__.py index d93bcd9..3d29e21 100644 --- a/dronecan_gui_tool/widgets/__init__.py +++ b/dronecan_gui_tool/widgets/__init__.py @@ -8,7 +8,7 @@ import os import re -import pkg_resources +from importlib import resources import queue from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QApplication, QWidget, \ QComboBox, QCompleter, QPushButton, QHBoxLayout, QVBoxLayout, QMessageBox @@ -640,7 +640,7 @@ def get_app_icon(): pass # noinspection PyBroadException try: - fn = pkg_resources.resource_filename('dronecan_gui_tool', os.path.join('icons', 'dronecan_gui_tool.png')) + fn = str(resources.files('dronecan_gui_tool').joinpath('icons', 'dronecan_gui_tool.png')) _APP_ICON_OBJECT = QIcon(fn) except Exception: logger.error('Could not load icon', exc_info=True) From a2653cff987f8a34e80c1d0a836053cd43ca3178 Mon Sep 17 00:00:00 2001 From: songmeo Date: Sat, 21 Mar 2026 20:20:10 +0000 Subject: [PATCH 2/8] Move pkg_resources import to Windows-only MSI build block pkg_resources is only used for unpacking eggs during cx_Freeze MSI builds. Importing it at module level causes ModuleNotFoundError on systems where setuptools is not installed. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index dea9620..3bfee4c 100755 --- a/setup.py +++ b/setup.py @@ -10,10 +10,8 @@ import os import sys import shutil -import pkg_resources import glob from setuptools import setup, find_packages -from setuptools.archive_util import unpack_archive PACKAGE_NAME = 'dronecan_gui_tool' HUMAN_FRIENDLY_NAME = 'DroneCAN GUI Tool' @@ -115,6 +113,8 @@ if ('bdist_msi' in sys.argv) or ('build_exe' in sys.argv): import cx_Freeze + import pkg_resources + from setuptools.archive_util import unpack_archive # cx_Freeze can't handle 3rd-party packages packed in .egg files, so we have to extract them for it dependency_eggs_to_unpack = [ From 883611cfe09d64ec609c71257697828936691797 Mon Sep 17 00:00:00 2001 From: songmeo Date: Sat, 21 Mar 2026 20:52:19 +0000 Subject: [PATCH 3/8] Use fully qualified importlib.resources calls and fix pip_sizes.py Address maintainer feedback: use import importlib.resources with fully qualified calls to avoid shadowing application variables. Also replace pkg_resources in pip_sizes.py with importlib.metadata. --- dronecan_gui_tool/widgets/__init__.py | 4 ++-- pip_sizes.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dronecan_gui_tool/widgets/__init__.py b/dronecan_gui_tool/widgets/__init__.py index 3d29e21..f87d9ff 100644 --- a/dronecan_gui_tool/widgets/__init__.py +++ b/dronecan_gui_tool/widgets/__init__.py @@ -8,7 +8,7 @@ import os import re -from importlib import resources +import importlib.resources import queue from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QApplication, QWidget, \ QComboBox, QCompleter, QPushButton, QHBoxLayout, QVBoxLayout, QMessageBox @@ -640,7 +640,7 @@ def get_app_icon(): pass # noinspection PyBroadException try: - fn = str(resources.files('dronecan_gui_tool').joinpath('icons', 'dronecan_gui_tool.png')) + fn = str(importlib.resources.files('dronecan_gui_tool').joinpath('icons', 'dronecan_gui_tool.png')) _APP_ICON_OBJECT = QIcon(fn) except Exception: logger.error('Could not load icon', exc_info=True) diff --git a/pip_sizes.py b/pip_sizes.py index e0b5226..e200b43 100644 --- a/pip_sizes.py +++ b/pip_sizes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import os -import pkg_resources +import importlib.metadata def calc_container(path): total_size = 0 @@ -13,7 +13,7 @@ def calc_container(path): -dists = [d for d in pkg_resources.working_set] +dists = list(importlib.metadata.distributions()) data = {} data2 = [] @@ -21,7 +21,7 @@ def calc_container(path): for dist in dists: try: - path = os.path.join(dist.location, dist.project_name) + path = os.path.join(str(dist._path.parent), dist.metadata['Name']) size = calc_container(path) if size/1000 > 1.0: #print (f"{dist}: {size/1000} KB") @@ -30,7 +30,7 @@ def calc_container(path): data2.append(a.split()[0])# first word data3[a.split()[0]] = f"{dist}: {size/1000} KB" except OSError: - '{} no longer exists'.format(dist.project_name) + '{} no longer exists'.format(dist.metadata['Name']) sorted_dict = dict(sorted(data.items())) sorted_dict2 = sorted(data2) From 1d41b6da834aa86cf7d567f94af5f0ba199b790c Mon Sep 17 00:00:00 2001 From: songmeo Date: Mon, 23 Mar 2026 13:15:56 +0000 Subject: [PATCH 4/8] Replace pkg_resources with importlib.metadata in Windows MSI build --- pip_sizes.py | 13 +++++++------ setup.py | 14 +++++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pip_sizes.py b/pip_sizes.py index e200b43..5f110bb 100644 --- a/pip_sizes.py +++ b/pip_sizes.py @@ -21,14 +21,15 @@ def calc_container(path): for dist in dists: try: - path = os.path.join(str(dist._path.parent), dist.metadata['Name']) + dist_name = dist.metadata.get('Name', 'unknown') + path = os.path.join(str(dist.locate_file('')), dist_name) + if not os.path.exists(path): + path = os.path.join(str(dist.locate_file('')), dist_name.replace('-', '_')) size = calc_container(path) if size/1000 > 1.0: - #print (f"{dist}: {size/1000} KB") - data[size] = f"{dist}: {size/1000} KB" - a = f"{dist}" - data2.append(a.split()[0])# first word - data3[a.split()[0]] = f"{dist}: {size/1000} KB" + data[size] = f"{dist_name}: {size/1000} KB" + data2.append(dist_name) + data3[dist_name] = f"{dist_name}: {size/1000} KB" except OSError: '{} no longer exists'.format(dist.metadata['Name']) diff --git a/setup.py b/setup.py index 3bfee4c..1a74f92 100755 --- a/setup.py +++ b/setup.py @@ -113,7 +113,7 @@ if ('bdist_msi' in sys.argv) or ('build_exe' in sys.argv): import cx_Freeze - import pkg_resources + import importlib.metadata from setuptools.archive_util import unpack_archive # cx_Freeze can't handle 3rd-party packages packed in .egg files, so we have to extract them for it @@ -130,9 +130,13 @@ except Exception: pass for dep in dependency_eggs_to_unpack: - for egg in pkg_resources.require(dep): - if not os.path.isdir(egg.location): - unpack_archive(egg.location, unpacked_eggs_dir) + try: + dist = importlib.metadata.distribution(dep) + except importlib.metadata.PackageNotFoundError: + continue + dist_location = str(dist.locate_file('')) + if not os.path.isdir(dist_location): + unpack_archive(dist_location, unpacked_eggs_dir) import qtawesome import qtconsole @@ -153,7 +157,7 @@ args['options'] = { 'build_exe': { 'packages': [ - 'pkg_resources', + 'importlib.metadata', 'zmq', 'pygments', 'jupyter_client', From 259ff35ec09fb1547884daa5a2e5ed0b84952103 Mon Sep 17 00:00:00 2001 From: songmeo Date: Mon, 23 Mar 2026 13:49:01 +0000 Subject: [PATCH 5/8] Use as_file context manager for icon resource loading --- dronecan_gui_tool/widgets/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dronecan_gui_tool/widgets/__init__.py b/dronecan_gui_tool/widgets/__init__.py index f87d9ff..ee2b61b 100644 --- a/dronecan_gui_tool/widgets/__init__.py +++ b/dronecan_gui_tool/widgets/__init__.py @@ -640,8 +640,9 @@ def get_app_icon(): pass # noinspection PyBroadException try: - fn = str(importlib.resources.files('dronecan_gui_tool').joinpath('icons', 'dronecan_gui_tool.png')) - _APP_ICON_OBJECT = QIcon(fn) + icon_resource = importlib.resources.files('dronecan_gui_tool').joinpath('icons', 'dronecan_gui_tool.png') + with importlib.resources.as_file(icon_resource) as icon_path: + _APP_ICON_OBJECT = QIcon(str(icon_path)) except Exception: logger.error('Could not load icon', exc_info=True) _APP_ICON_OBJECT = QIcon() From c7fe731ef24cf1669e40bf9022daf8a74a38830b Mon Sep 17 00:00:00 2001 From: songmeo Date: Mon, 23 Mar 2026 13:50:35 +0000 Subject: [PATCH 6/8] Fix no-op format string in pip_sizes.py error handler --- pip_sizes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip_sizes.py b/pip_sizes.py index 5f110bb..35daaf5 100644 --- a/pip_sizes.py +++ b/pip_sizes.py @@ -31,7 +31,7 @@ def calc_container(path): data2.append(dist_name) data3[dist_name] = f"{dist_name}: {size/1000} KB" except OSError: - '{} no longer exists'.format(dist.metadata['Name']) + pass sorted_dict = dict(sorted(data.items())) sorted_dict2 = sorted(data2) From fbd8f1c461d65dd95ef2d23f79fb9125345b8075 Mon Sep 17 00:00:00 2001 From: songmeo Date: Mon, 23 Mar 2026 13:57:14 +0000 Subject: [PATCH 7/8] Show package version in pip_sizes.py output --- pip_sizes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pip_sizes.py b/pip_sizes.py index 35daaf5..8f80347 100644 --- a/pip_sizes.py +++ b/pip_sizes.py @@ -27,9 +27,10 @@ def calc_container(path): path = os.path.join(str(dist.locate_file('')), dist_name.replace('-', '_')) size = calc_container(path) if size/1000 > 1.0: - data[size] = f"{dist_name}: {size/1000} KB" + dist_label = f"{dist_name} {dist.metadata.get('Version', '')}" + data[size] = f"{dist_label}: {size/1000} KB" data2.append(dist_name) - data3[dist_name] = f"{dist_name}: {size/1000} KB" + data3[dist_name] = f"{dist_label}: {size/1000} KB" except OSError: pass From c97a6d1a949e9bbc216ac46a8987f92f0b9f5ffb Mon Sep 17 00:00:00 2001 From: songmeo Date: Mon, 23 Mar 2026 14:08:54 +0000 Subject: [PATCH 8/8] Fix multiprocessing start method check for Debian 13+ --- dronecan_gui_tool/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dronecan_gui_tool/main.py b/dronecan_gui_tool/main.py index 611ffc2..433594e 100644 --- a/dronecan_gui_tool/main.py +++ b/dronecan_gui_tool/main.py @@ -60,7 +60,7 @@ # Start method must be configured globally, and only once. Using 'spawn' ensures full compatibility with Windoze. # We need to check first if the start mode is already configured, because this code will be re-run for every child. # -if multiprocessing.get_start_method(True) != 'spawn': +if multiprocessing.get_start_method(allow_none=True) is None: multiprocessing.set_start_method('spawn') #