From 6a6e13ffeda31256b0594c8f8857805fd84c73f1 Mon Sep 17 00:00:00 2001 From: Kye Morton Date: Sat, 7 Sep 2024 15:09:53 +1000 Subject: [PATCH 1/2] Conversion to ROS2 package --- CMakeLists.txt | 22 ----- package.xml | 21 ++-- setup.py | 43 +++++++-- src/rqt_dep/dotcode_pack.py | 177 ++++++++++++++++++++++------------ src/rqt_dep/ros_pack_graph.py | 47 +++++---- 5 files changed, 182 insertions(+), 128 deletions(-) delete mode 100644 CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 24c479f..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -cmake_minimum_required(VERSION 3.0.2) -project(rqt_dep) -# Load catkin and all dependencies required for this package -find_package(catkin REQUIRED) -catkin_package() -catkin_python_setup() - -if(CATKIN_ENABLE_TESTING) - catkin_add_nosetests(test/dotcode_pack_test.py) -endif() - -install(FILES plugin.xml - DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} -) - -install(DIRECTORY resource - DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} -) - -catkin_install_python(PROGRAMS scripts/rqt_dep - DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} -) diff --git a/package.xml b/package.xml index 58f8238..c104d51 100644 --- a/package.xml +++ b/package.xml @@ -18,24 +18,17 @@ Dirk Thomas Thibault Kruse - catkin - python-setuptools - python3-setuptools - - python_qt_binding - python-rospkg - python3-rospkg - qt_dotgraph - qt_gui + ament_index_python + python_qt_binding + qt_dotgraph + rqt_gui + rqt_gui_py + python3-rospkg qt_gui_py_common rqt_graph - rqt_gui_py - - python-mock - python3-mock - + ament_python diff --git a/setup.py b/setup.py index ebafa82..13a22b9 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,39 @@ -#!/usr/bin/env python - from setuptools import setup -from catkin_pkg.python_setup import generate_distutils_setup -d = generate_distutils_setup( - packages=['rqt_dep'], +package_name = 'rqt_dep' +setup( + name=package_name, + version='1.3.1', package_dir={'': 'src'}, - scripts=['scripts/rqt_dep'] + packages=[package_name], + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name + '/resource', ['resource/RosPackGraph.ui']), + ('share/' + package_name, ['package.xml']), + ('share/' + package_name, ['plugin.xml']), + ('lib/' + package_name, ['scripts/rqt_dep']), + ], + install_requires=['setuptools'], + zip_safe=True, + author='Dirk Thomas', + maintainer='Dirk Thomas, Aaron Blasdel, Thibault Kruse', + maintainer_email='dthomas@osrfoundation.org', + keywords=['ROS'], + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python', + 'Topic :: Software Development', + ], + description=( + 'rqt_dep provides a Python GUI plugin to visualize a ROS package network..' + ), + license='BSD', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'rqt_dep = rqt_dep.main:main', + ], + }, ) - -setup(**d) diff --git a/src/rqt_dep/dotcode_pack.py b/src/rqt_dep/dotcode_pack.py index 83971a0..9ea1d04 100644 --- a/src/rqt_dep/dotcode_pack.py +++ b/src/rqt_dep/dotcode_pack.py @@ -32,13 +32,21 @@ from __future__ import with_statement, print_function -import os +import xml.etree.ElementTree as ET + import re +from typing import Any, Callable, List +from typing_extensions import Protocol + +class GetItem(Protocol): + def __getitem__(self: 'Getitem', key: Any) -> Any: pass # type: ignore + def __getitem__(self: 'Getitem', key: Any) -> Any: pass # type: ignore -from rospkg import MANIFEST_FILE -from rospkg.common import ResourceNotFound +from rospkg.common import PACKAGE_FILE from qt_dotgraph.colors import get_color_for_string +from ament_index_python.packages import get_search_paths, get_package_prefix, get_package_share_path, PackageNotFoundError +import apt def matches_any(name, patternlist): for pattern in patternlist: @@ -51,16 +59,17 @@ def matches_any(name, patternlist): class RosPackageGraphDotcodeGenerator: - - def __init__(self, rospack, rosstack): + def __init__(self, list_fun:Callable[[],List[str]], system_cache:GetItem): """ :param rospack: use rospkg.RosPack() :param rosstack: use rospkg.RosStack() """ - self.rospack = rospack - self.rosstack = rosstack - self.stacks = {} - self.packages = {} + # self.rospack = rospack + # self.rosstack = rosstack + self.list_fun = list_fun + self.system_cache = system_cache + # self.stacks = {} + # self.packages = {} self.package_types = {} self.edges = {} self.traversed_ancestors = {} @@ -68,6 +77,9 @@ def __init__(self, rospack, rosstack): self.last_drawargs = None self.last_selection = None + search_paths = get_search_paths() + self.dry_path = get_search_paths()[-1] if len(search_paths) else "" + def generate_dotcode(self, dotcode_factory, selected_names=None, @@ -150,26 +162,26 @@ def generate_dotcode(self, self.traversed_ancestors = {} self.traversed_descendants = {} # update internal graph structure - for name in self.rospack.list(): + for name in self.list_fun(): if matches_any(name, self.selected_names): if descendants: self.add_package_descendants_recursively(name) if ancestors: self.add_package_ancestors_recursively(name) - for stackname in self.rosstack.list(): - if matches_any(stackname, self.selected_names): - manifest = self.rosstack.get_manifest(stackname) - if manifest.is_catkin: - if descendants: - self.add_package_descendants_recursively(stackname) - if ancestors: - self.add_package_ancestors_recursively(stackname) - else: - for package_name in self.rosstack.packages_of(stackname): - if descendants: - self.add_package_descendants_recursively(package_name) - if ancestors: - self.add_package_ancestors_recursively(package_name) + # for stackname in self.rosstack.list(): + # if matches_any(stackname, self.selected_names): + # manifest = self.rosstack.get_manifest(stackname) + # if manifest.is_wet: + # if descendants: + # self.add_package_descendants_recursively(stackname) + # if ancestors: + # self.add_package_ancestors_recursively(stackname) + # else: + # for package_name in self.rosstack.packages_of(stackname): + # if descendants: + # self.add_package_descendants_recursively(package_name) + # if ancestors: + # self.add_package_ancestors_recursively(package_name) drawing_args = { 'dotcode_factory': dotcode_factory, @@ -246,11 +258,11 @@ def _generate_package(self, dotcode_factory, graph, package_name, attributes=Non if self.mark_selected and \ '.*' not in self.selected_names and \ matches_any(package_name, self.selected_names): - if attributes and attributes['is_catkin']: + if attributes and attributes['is_wet']: color = 'red' else: color = 'tomato' - elif attributes and not attributes['is_catkin']: + elif attributes and not attributes['is_wet']: color = 'gray' if attributes and 'not_found' in attributes and attributes['not_found']: color = 'orange' @@ -272,23 +284,23 @@ def _add_package(self, package_name, parent=None): if package_name in self.packages: return False - catkin_package = self._is_package_wet(package_name) - if catkin_package is None: + wet_package = self._is_package_wet(package_name) + if wet_package is None: return False - self.packages[package_name] = {'is_catkin': catkin_package} - - if self.with_stacks: - try: - stackname = self.rospack.stack_of(package_name) - except ResourceNotFound as e: - print( - 'RosPackageGraphDotcodeGenerator._add_package(%s), ' - 'parent %s: ResourceNotFound:' % (package_name, parent), e) - stackname = None - if not stackname is None and stackname != '': - if not stackname in self.stacks: - self._add_stack(stackname) - self.stacks[stackname]['packages'].append(package_name) + self.packages[package_name] = {'is_wet': wet_package} + + # if self.with_stacks: + # try: + # stackname = self.rospack.stack_of(package_name) + # except ResourceNotFound as e: + # print( + # 'RosPackageGraphDotcodeGenerator._add_package(%s), ' + # 'parent %s: ResourceNotFound:' % (package_name, parent), e) + # stackname = None + # if not stackname is None and stackname != '': + # if not stackname in self.stacks: + # self._add_stack(stackname) + # self.stacks[stackname]['packages'].append(package_name) return True def _hide_package(self, package_name): @@ -305,10 +317,10 @@ def _hide_package(self, package_name): def _is_package_wet(self, package_name): if package_name not in self.package_types: try: - package_path = self.rospack.get_path(package_name) - manifest_file = os.path.join(package_path, MANIFEST_FILE) - self.package_types[package_name] = not os.path.exists(manifest_file) - except ResourceNotFound: + # package_path = self.rospack.get_path(package_name) + # manifest_file = os.path.join(package_path, MANIFEST_FILE) + self.package_types[package_name] = get_package_prefix(package_name) != self.dry_path # not os.path.exists(manifest_file) + except PackageNotFoundError: return None return self.package_types[package_name] @@ -317,6 +329,29 @@ def _add_edge(self, name1, name2, attributes=None): return self.edges[(name1, name2)] = attributes + @staticmethod + def _get_depends_on(package_name:str): + r = get_package_share_path(package_name) / PACKAGE_FILE + tree = ET.parse(r) + dep_types = [ + "build_depend", + # "build_export_depend", + # "buildtool_depend", + # "buildtool_export_depend", + "exec_depend", + "depend", + # "doc_depend", + # "test_depend", + ] + + deps:set[str] = set() + + for dep_t in dep_types: + for dep in [x.text for x in tree.iter(dep_t) if x.text]: + deps.add(dep) + + return deps + def add_package_ancestors_recursively( self, package_name, expanded_up=None, depth=None, implicit=False, parent=None): @@ -347,8 +382,8 @@ def add_package_ancestors_recursively( expanded_up.append(package_name) if (depth != 1): try: - depends_on = self.rospack.get_depends_on(package_name, implicit=implicit) - except ResourceNotFound as e: + depends_on = RosPackageGraphDotcodeGenerator._get_depends_on(package_name) + except PackageNotFoundError as e: print( 'RosPackageGraphDotcodeGenerator.add_package_ancestors_recursively(%s),' ' parent %s: ResourceNotFound:' % (package_name, parent), e) @@ -389,29 +424,43 @@ def add_package_descendants_recursively( expanded = [] expanded.append(package_name) if (depth != 1): + is_ros_package = False + is_system_package = False + search_problem = '???' try: - try: - depends = self.rospack.get_depends(package_name, implicit=implicit) - except ResourceNotFound: - # try falling back to rosstack to find wet metapackages - manifest = self.rosstack.get_manifest(package_name) - if manifest.is_catkin: - depends = [d.name for d in manifest.depends] - else: - raise - except ResourceNotFound as e: - print( - 'RosPackageGraphDotcodeGenerator.add_package_descendants_recursively(%s), ' - 'parent: %s: ResourceNotFound:' % (package_name, parent), e) + # try: + depends = RosPackageGraphDotcodeGenerator._get_depends_on(package_name) #self.rospack.get_depends(package_name, implicit=implicit) + is_ros_package = True + # except ResourceNotFound: + # # try falling back to rosstack to find wet metapackages + # manifest = self.rosstack.get_manifest(package_name) + # if manifest.is_wet: + # depends = [d.name for d in manifest.depends] + # else: + # raise + except PackageNotFoundError: depends = [] + search_problem = f"Resource not found in: {get_search_paths()}" + # get system dependencies without recursion - if self.show_system: - rosdeps = self.rospack.get_rosdeps(package_name, implicit=implicit) - for dep_name in [x for x in rosdeps if not matches_any(x, self.excludes)]: + if self.show_system and not is_ros_package: + + if any(key.includes(package_name) for key in self.system_cache): + # rosdeps = self.rospack.get_rosdeps(package_name, implicit=implicit) + # for dep_name in [x for x in rosdeps if not matches_any(x, self.excludes)]: + dep_name = package_name if not self.hide_transitives or not dep_name in expanded: self._add_edge(package_name, dep_name) self._add_package(dep_name, parent=package_name) expanded.append(dep_name) + else: + search_problem = f"Resource not found in system" + + if not is_ros_package and not is_system_package: + print( + 'RosPackageGraphDotcodeGenerator.add_package_descendants_recursively(%s), ' + 'parent: %s: ' % (package_name, parent), search_problem) + new_nodes = [] for dep_name in [x for x in depends if not matches_any(x, self.excludes)]: if not self.hide_transitives or not dep_name in expanded: diff --git a/src/rqt_dep/ros_pack_graph.py b/src/rqt_dep/ros_pack_graph.py index 5c65c1c..0d0a275 100644 --- a/src/rqt_dep/ros_pack_graph.py +++ b/src/rqt_dep/ros_pack_graph.py @@ -35,8 +35,13 @@ import os import pickle import sys +from typing import List, cast + +import apt + +from ros2pkg.api import get_package_names +from ament_index_python.packages import get_package_share_directory -import rospkg from python_qt_binding import loadUi from python_qt_binding.QtCore import QFile, QIODevice, Qt, Signal, Slot, QAbstractListModel @@ -44,8 +49,8 @@ from python_qt_binding.QtWidgets import QFileDialog, QGraphicsScene, QWidget, QCompleter from python_qt_binding.QtSvg import QSvgGenerator -import rosservice -import rostopic +from ros2service.api import get_service_names_and_types +from ros2topic.api import get_msg_class from .dotcode_pack import RosPackageGraphDotcodeGenerator from qt_dotgraph.pydotfactory import PydotFactory @@ -77,9 +82,9 @@ class StackageCompletionModel(QAbstractListModel): """Ros package and stacknames""" - def __init__(self, linewidget, rospack, rosstack): + def __init__(self, linewidget): super(StackageCompletionModel, self).__init__(linewidget) - self.allnames = sorted(list(set(list(rospack.list()) + list(rosstack.list())))) + self.allnames = cast(List[str], sorted(get_package_names())) self.allnames = self.allnames + ['-%s' % name for name in self.allnames] def rowCount(self, parent): @@ -108,20 +113,19 @@ def __init__(self, context): self.setObjectName('RosPackGraph') - rospack = rospkg.RosPack() - rosstack = rospkg.RosStack() + # rospack = rospkg.RosPack() + # rosstack = rospkg.RosStack() # factory builds generic dotcode items self.dotcode_factory = PydotFactory() # self.dotcode_factory = PygraphvizFactory() # generator builds rosgraph - self.dotcode_generator = RosPackageGraphDotcodeGenerator(rospack, rosstack) + self.dotcode_generator = RosPackageGraphDotcodeGenerator(lambda: list(get_package_names()), apt.Cache()) # dot_to_qt transforms into Qt elements using dot layout self.dot_to_qt = DotToQtGenerator() self._widget = QWidget() - rp = rospkg.RosPack() - ui_file = os.path.join(rp.get_path('rqt_dep'), 'resource', 'RosPackGraph.ui') + ui_file = os.path.join(get_package_share_directory('rqt_dep'), 'resource', 'RosPackGraph.ui') loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView}) self._widget.setObjectName('RosPackGraphUi') if context.serial_number() > 1: @@ -149,7 +153,7 @@ def __init__(self, context): self._widget.package_type_combo_box.insertItem(2, self.tr('dry only'), 1) self._widget.package_type_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph) - completionmodel = StackageCompletionModel(self._widget.filter_line_edit, rospack, rosstack) + completionmodel = StackageCompletionModel(self._widget.filter_line_edit) completer = RepeatedWordCompleter(completionmodel, self) completer.setCompletionMode(QCompleter.PopupCompletion) completer.setWrapAround(True) @@ -323,6 +327,7 @@ def _generate_dotcode(self): excludes.append(name.strip()[1:]) else: includes.append(name.strip()) + # orientation = 'LR' descendants = True ancestors = True @@ -353,21 +358,23 @@ def _update_graph(self, dotcode): def _generate_tool_tip(self, url): if url is not None and ':' in url: + print(url) item_type, item_path = url.split(':', 1) if item_type == 'node': tool_tip = 'Node:\n %s' % (item_path) - service_names = rosservice.get_service_list(node=item_path) - if service_names: + services = get_service_names_and_types(node=item_path) + if services: tool_tip += '\nServices:' - for service_name in service_names: - try: - service_type = rosservice.get_service_type(service_name) - tool_tip += '\n %s [%s]' % (service_name, service_type) - except rosservice.ROSServiceIOException as e: - tool_tip += '\n %s' % (e) + for service_name, service_type in services: + # try: + # service_type = rosservice.get_service_type(service_name) + tool_tip += '\n %s [%s]' % (service_name, service_type) + # except rosservice.ROSServiceIOException as e: + # tool_tip += '\n %s' % (e) return tool_tip elif item_type == 'topic': - topic_type, topic_name, _ = rostopic.get_topic_type(item_path) + print(item_path) + topic_type, topic_name, _ = get_msg_class(item_path, item_path) return 'Topic:\n %s\nType:\n %s' % (topic_name, topic_type) return url From c78b4573a333be0f8e0712d86f7f1814feaf2baf Mon Sep 17 00:00:00 2001 From: Kye Morton Date: Sat, 7 Sep 2024 15:10:36 +1000 Subject: [PATCH 2/2] Additional files --- pytest.ini | 2 ++ resource/rqt_dep | 0 2 files changed, 2 insertions(+) create mode 100644 pytest.ini create mode 100644 resource/rqt_dep diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..50d6d01 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +junit_family=xunit2 \ No newline at end of file diff --git a/resource/rqt_dep b/resource/rqt_dep new file mode 100644 index 0000000..e69de29