diff --git a/package.xml b/package.xml index 22e4c3a..53dc465 100644 --- a/package.xml +++ b/package.xml @@ -14,6 +14,8 @@ https://github.com/ros-visualization/rqt_tf_tree https://github.com/ros-visualization/rqt_tf_tree/issues + ament_flake8 + ament_pep257 python3-pytest python_qt_binding diff --git a/setup.cfg b/setup.cfg index e6b74bf..049d8b5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,3 +2,8 @@ script_dir=$base/lib/rqt_tf_tree [install] install_scripts=$base/lib/rqt_tf_tree +[tool:pytest] +filterwarnings = + # flake8 spawns multiprocessing 'fork' workers while colcon's test + # executor is multi-threaded, which CPython >=3.12 warns about. Harmless. + ignore:This process:DeprecationWarning diff --git a/setup.py b/setup.py index 56b7faf..13476c9 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,11 @@ 'rqt_tf_tree provides a GUI plugin for visualizing the ROS TF frame tree.' ), license='BSD', - tests_require=['pytest'], + extras_require={ + 'test': [ + 'pytest', + ], + }, entry_points={ 'console_scripts': [ 'rqt_tf_tree = ' + package_name + '.main:main' diff --git a/src/rqt_tf_tree/dotcode_tf.py b/src/rqt_tf_tree/dotcode_tf.py index 878f2e0..719527e 100644 --- a/src/rqt_tf_tree/dotcode_tf.py +++ b/src/rqt_tf_tree/dotcode_tf.py @@ -31,20 +31,17 @@ # POSSIBILITY OF SUCH DAMAGE. import time -import rclpy -import yaml from rclpy import clock from rclpy.constants import S_TO_NS from tf2_msgs.srv import FrameGraph +import yaml class RosTfTreeDotcodeGenerator(object): def __init__(self, initial_listen_duration=1): - """ - :param initial_listen_duration: how many secs to listen to tf initially. - """ + """:param initial_listen_duration: how many secs to listen to tf initially.""" self.last_drawargs = None self.dotcode = None self.firstcall = True @@ -58,15 +55,13 @@ def __init__(self, initial_listen_duration=1): def generate_dotcode(self, dotcode_factory, tf2_frame_srv, - timer=rclpy.clock.Clock(), + timer=clock.Clock(), yaml_parser=yaml, rank='same', # None, same, min, max, source, sink ranksep=0.2, # vertical distance between layers rankdir='TB', # direction of layout (TB top > bottom, LR left > right) force_refresh=False): - """ - :param force_refresh: if False, may return same dotcode as last time - """ + """:param force_refresh: if False, may return same dotcode as last time.""" if self.firstcall is True: self.firstcall = False force_refresh = True @@ -131,7 +126,8 @@ def generate(self, data, timestamp): edge_label = '"Broadcaster: %s\\n' % str(tf_frame_values['broadcaster']) edge_label += 'Average rate: %s\\n' % str(tf_frame_values['rate']) edge_label += 'Buffer length: %s\\n' % str(tf_frame_values['buffer_length']) - edge_label += 'Most recent transform: %s\\n' % str(tf_frame_values['most_recent_transform']) + edge_label += 'Most recent transform: %s\\n' % \ + str(tf_frame_values['most_recent_transform']) edge_label += 'Oldest transform: %s"' % str(tf_frame_values['oldest_transform']) self.dotcode_factory.add_edge_to_graph(graph, str(tf_frame_values['parent']), @@ -146,7 +142,8 @@ def generate(self, data, timestamp): root, style='invis') - # dot += ' subgraph cluster_legend { style=bold; color=black; label ="view_frames Result";\n' + # dot += ' subgraph cluster_legend { style=bold; color=black;' + # dot += ' label ="view_frames Result";\n' # dot += '"Recorded at time: '+str(rospy.Time.now().to_sec())+'"[ shape=plaintext ] ;\n' # dot += '}->"'+root+'"[style=invis];\n}' return graph diff --git a/src/rqt_tf_tree/tf_tree.py b/src/rqt_tf_tree/tf_tree.py index a2ee7fe..e514324 100644 --- a/src/rqt_tf_tree/tf_tree.py +++ b/src/rqt_tf_tree/tf_tree.py @@ -34,18 +34,17 @@ from ament_index_python import get_resource -from tf2_msgs.srv import FrameGraph -import tf2_ros - from python_qt_binding import loadUi from python_qt_binding.QtCore import QFile, QIODevice, QObject, Qt, Signal from python_qt_binding.QtGui import QIcon, QImage, QPainter -from python_qt_binding.QtWidgets import QFileDialog, QGraphicsScene, QWidget from python_qt_binding.QtSvg import QSvgGenerator -from qt_dotgraph.pydotfactory import PydotFactory -# from qt_dotgraph.pygraphvizfactory import PygraphvizFactory +from python_qt_binding.QtWidgets import QFileDialog, QGraphicsScene, QWidget from qt_dotgraph.dot_to_qt import DotToQtGenerator +# from qt_dotgraph.pygraphvizfactory import PygraphvizFactory +from qt_dotgraph.pydotfactory import PydotFactory from rqt_graph.interactive_graphics_view import InteractiveGraphicsView +from tf2_msgs.srv import FrameGraph +import tf2_ros from .dotcode_tf import RosTfTreeDotcodeGenerator @@ -82,7 +81,8 @@ def __init__(self, context): loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView}) self._widget.setObjectName('RosTfTreeUi') if context.serial_number() > 1: - self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number())) + self._widget.setWindowTitle( + self._widget.windowTitle() + (' (%d)' % context.serial_number())) self._scene = QGraphicsScene() self._scene.setBackgroundBrush(Qt.white) @@ -126,7 +126,8 @@ def restore_settings(self, plugin_settings, instance_settings): self._widget.auto_fit_graph_check_box.setChecked( instance_settings.value('auto_fit_graph_check_box_state', True) in [True, 'true']) self._widget.highlight_connections_check_box.setChecked( - instance_settings.value('highlight_connections_check_box_state', True) in [True, 'true']) + instance_settings.value( + 'highlight_connections_check_box_state', True) in [True, 'true']) self.initialized = True self._refresh_tf_graph() diff --git a/test/dotcode_tf_test.py b/test/dotcode_tf_test.py index 932c707..1458f83 100644 --- a/test/dotcode_tf_test.py +++ b/test/dotcode_tf_test.py @@ -32,9 +32,6 @@ # POSSIBILITY OF SUCH DAMAGE. import unittest - -import rclpy.client - from unittest.mock import Mock from rqt_tf_tree.dotcode_tf import RosTfTreeDotcodeGenerator @@ -44,11 +41,11 @@ class DotcodeGeneratorTest(unittest.TestCase): def test_generate_dotcode(self): yaml_data = {'frame1': {'parent': 'fr_parent', - 'broadcaster': 'fr_broadcaster', - 'rate': 'fr_rate', - 'buffer_length': 'fr_buffer_length', - 'most_recent_transform': 'fr_most_recent_transform', - 'oldest_transform': 'fr_oldest_transform',}} + 'broadcaster': 'fr_broadcaster', + 'rate': 'fr_rate', + 'buffer_length': 'fr_buffer_length', + 'most_recent_transform': 'fr_most_recent_transform', + 'oldest_transform': 'fr_oldest_transform'}} frameClientMock = Mock() frameClientMock.call.return_value.frame_yaml = str(yaml_data) @@ -60,7 +57,7 @@ def test_generate_dotcode(self): yamlmock = Mock() yamlmock.load.return_value = yaml_data - factoryMock.create_dot.return_value = "foo" + factoryMock.create_dot.return_value = 'foo' factoryMock.get_graph.return_value = graphMock gen = RosTfTreeDotcodeGenerator(0) @@ -74,4 +71,3 @@ def test_generate_dotcode(self): if __name__ == '__main__': unittest.main() - diff --git a/test/test_flake8.py b/test/test_flake8.py new file mode 100644 index 0000000..2fb0b55 --- /dev/null +++ b/test/test_flake8.py @@ -0,0 +1,41 @@ +# Copyright (c) 2026, Open Source Robotics Foundation, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/test/test_pep257.py b/test/test_pep257.py new file mode 100644 index 0000000..3dbe009 --- /dev/null +++ b/test/test_pep257.py @@ -0,0 +1,39 @@ +# Copyright (c) 2026, Open Source Robotics Foundation, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings'