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'