diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3442bc7 --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +--- +Language: Cpp +BasedOnStyle: Google +ColumnLimit: 120 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..149a0b0 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Add git commit hashes to ignore for blame +0da6a5e499812b03d4135ff32a9b1ab01bd07ed9 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..74a598c --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,16 @@ +name: Code style checks + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - name: Install cppcheck + run: sudo apt install cppcheck -y + - uses: pre-commit/action@v3.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7bbe0a1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.277 + hooks: + - id: ruff + args: + - "--fix" + - "--exit-non-zero-on-fix" +- repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black +- repo: https://github.com/pocc/pre-commit-hooks + rev: v1.3.5 + hooks: + - id: clang-format + args: + - "-i" + - id: cppcheck + args: + - "--suppress=missingInclude" + - "--suppress=unmatchedSuppression" + - "--suppress=unusedFunction" diff --git a/docs/conf.py b/docs/conf.py index 586c773..8bd6bd3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Full list of options at http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- @@ -10,18 +8,20 @@ # import os import sys -import catkin_pkg.package +import catkin_pkg.package from exhale import utils package_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) catkin_package = catkin_pkg.package.parse_package( - os.path.join(package_dir, catkin_pkg.package.PACKAGE_MANIFEST_FILENAME)) + os.path.join(package_dir, catkin_pkg.package.PACKAGE_MANIFEST_FILENAME) +) sys.path.insert(0, os.path.abspath(os.path.join(package_dir, "src"))) # -- Helper functions -------------------------------------------------------- + def count_files(): """:returns tuple of (num_py, num_cpp)""" num_py = 0 @@ -42,7 +42,7 @@ def count_files(): # -- Project information ----------------------------------------------------- project = catkin_package.name -copyright = '2019, Bit-Bots' +copyright = "2019, Bit-Bots" author = ", ".join([a.name for a in catkin_package.authors]) # The short X.Y version @@ -60,27 +60,27 @@ def count_files(): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.imgmath', - 'sphinx.ext.viewcode', - 'sphinx_rtd_theme', + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.imgmath", + "sphinx.ext.viewcode", + "sphinx_rtd_theme", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -92,7 +92,7 @@ def count_files(): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None @@ -108,24 +108,17 @@ def count_files(): if num_files_cpp > 0: extensions += [ - 'breathe', - 'exhale', + "breathe", + "exhale", ] - breathe_projects = { - project: os.path.join("_build", "doxyoutput", "xml") - } + breathe_projects = {project: os.path.join("_build", "doxyoutput", "xml")} breathe_default_project = project def specifications_for_kind(kind): # Show all members for classes and structs if kind == "class" or kind == "struct": - return [ - ":members:", - ":protected-members:", - ":private-members:", - ":undoc-members:" - ] + return [":members:", ":protected-members:", ":private-members:", ":undoc-members:"] # An empty list signals to Exhale to use the defaults else: return [] @@ -136,13 +129,11 @@ def specifications_for_kind(kind): "rootFileName": "library_root.rst", "rootFileTitle": "C++ Library API", "doxygenStripFromPath": "..", - "customSpecificationsMapping": utils.makeCustomSpecificationsMapping( - specifications_for_kind - ), + "customSpecificationsMapping": utils.makeCustomSpecificationsMapping(specifications_for_kind), # Suggested optional arguments "createTreeView": True, "exhaleExecutesDoxygen": True, - "exhaleDoxygenStdin": "INPUT = {}".format(os.path.join(package_dir, "include")) + "exhaleDoxygenStdin": "INPUT = {}".format(os.path.join(package_dir, "include")), } # -- Options for HTML output ------------------------------------------------- @@ -150,7 +141,7 @@ def specifications_for_kind(kind): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -161,7 +152,7 @@ def specifications_for_kind(kind): # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -173,14 +164,14 @@ def specifications_for_kind(kind): # # html_sidebars = {} -html_logo = os.path.join('_static', 'logo.png') -html_favicon = os.path.join('_static', 'logo.png') +html_logo = os.path.join("_static", "logo.png") +html_favicon = os.path.join("_static", "logo.png") # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {"https://docs.python.org/": None} # -- Options for todo extension ---------------------------------------------- @@ -188,6 +179,8 @@ def specifications_for_kind(kind): todo_include_todos = True # -- RST Standard variables --------------------------------------------------- -rst_prolog = ".. |project| replace:: {}\n".format(project) +rst_prolog = f".. |project| replace:: {project}\n" rst_prolog += ".. |description| replace:: {}\n".format(catkin_package.description.replace("\n\n", "\n")) -rst_prolog += ".. |modindex| replace:: {}\n".format(":ref:`modindex`" if num_files_py > 0 else "Python module index is not available") +rst_prolog += ".. |modindex| replace:: {}\n".format( + ":ref:`modindex`" if num_files_py > 0 else "Python module index is not available" +) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..437aa88 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.black] +line-length = 120 + +[tool.ruff] +line-length = 120 +select = ["F", "E", "W", "I", "N", "UP"] diff --git a/setup.py b/setup.py index 28a42a7..9ece56e 100644 --- a/setup.py +++ b/setup.py @@ -1,31 +1,29 @@ import glob -from setuptools import find_packages -from setuptools import setup +from setuptools import find_packages, setup -package_name = 'udp_bridge' +package_name = "udp_bridge" setup( - name=package_name, - packages=find_packages(exclude=['test']), - data_files=[ - ('share/ament_index/resource_index/packages', - ['resource/' + package_name]), - ('share/' + package_name, ['package.xml']), - ('share/' + package_name + '/config', glob.glob('config/*.yaml')), - ('share/' + package_name + '/launch', glob.glob('launch/*.launch')), - ], - install_requires=[ - 'launch', - 'setuptools', - ], - zip_safe=True, - keywords=['ROS'], - license='MIT', - entry_points={ - 'console_scripts': [ - f'receiver = {package_name}.receiver:main', - f'sender = {package_name}.sender:main', - ], - } + name=package_name, + packages=find_packages(exclude=["test"]), + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + ("share/" + package_name + "/config", glob.glob("config/*.yaml")), + ("share/" + package_name + "/launch", glob.glob("launch/*.launch")), + ], + install_requires=[ + "launch", + "setuptools", + ], + zip_safe=True, + keywords=["ROS"], + license="MIT", + entry_points={ + "console_scripts": [ + f"receiver = {package_name}.receiver:main", + f"sender = {package_name}.sender:main", + ], + }, ) diff --git a/test/rostests/test_sender.py b/test/rostests/test_sender.py index 536d44d..fa112db 100755 --- a/test/rostests/test_sender.py +++ b/test/rostests/test_sender.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -import rospy import socket -from std_msgs import msg + +import rospy from bitbots_test.test_case import RosNodeTestCase +from std_msgs import msg class SenderTestCase(RosNodeTestCase): @@ -24,4 +25,5 @@ def test_topic_gets_published_and_sent(self): if __name__ == "__main__": from bitbots_test import run_rostests + run_rostests(SenderTestCase) diff --git a/test/rostests/test_sender_receiver.py b/test/rostests/test_sender_receiver.py index ebdf088..cf4fe80 100755 --- a/test/rostests/test_sender_receiver.py +++ b/test/rostests/test_sender_receiver.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -import rospy from socket import gethostname -from std_msgs import msg -from bitbots_test.test_case import RosNodeTestCase + +import rospy from bitbots_test.mocks import MockSubscriber +from bitbots_test.test_case import RosNodeTestCase +from std_msgs import msg class SenderReceiverTestCase(RosNodeTestCase): @@ -23,4 +24,5 @@ def test_sent_message_gets_received_over_bridge(self): if __name__ == "__main__": from bitbots_test import run_rostests + run_rostests(SenderReceiverTestCase) diff --git a/test/unit_tests/test_aes_helper.py b/test/unit_tests/test_aes_helper.py index 55c5b5e..5452ab2 100644 --- a/test/unit_tests/test_aes_helper.py +++ b/test/unit_tests/test_aes_helper.py @@ -1,7 +1,8 @@ -from udp_bridge import aes_helper -from hypothesis import given, assume -from hypothesis.strategies import text from bitbots_test.test_case import TestCase +from hypothesis import assume, given +from hypothesis.strategies import text + +from udp_bridge import aes_helper class AesHelperTestCase(TestCase): @@ -15,6 +16,7 @@ def test_decrypt_inverts_encrypt(self, message, key): self.assertEqual(message, dec_text) -if __name__ == '__main__': +if __name__ == "__main__": from bitbots_test import run_unit_tests + run_unit_tests(AesHelperTestCase) diff --git a/udp_bridge/aes_helper.py b/udp_bridge/aes_helper.py index 87f216a..e80daef 100644 --- a/udp_bridge/aes_helper.py +++ b/udp_bridge/aes_helper.py @@ -1,5 +1,4 @@ import base64 -from typing import Optional from cryptography.fernet import Fernet from cryptography.hazmat.backends import default_backend @@ -15,7 +14,7 @@ class AESCipher: It is safe to keep one object because the internal python cipher is not reused. """ - def __init__(self, key: Optional[str]): + def __init__(self, key: str | None): """ :param key: The passphrase used to encrypt and decrypt messages. If it is None, no encryption/decryption takes place diff --git a/udp_bridge/message_handler.py b/udp_bridge/message_handler.py index 2fc1338..b033ac8 100644 --- a/udp_bridge/message_handler.py +++ b/udp_bridge/message_handler.py @@ -1,14 +1,13 @@ import base64 import pickle -from typing import Optional from udp_bridge.aes_helper import AESCipher class MessageHandler: PACKAGE_DELIMITER = b"\xff\xff\xff" - def __init__(self, encryption_key: Optional[str]): + def __init__(self, encryption_key: str | None): self.cipher = AESCipher(encryption_key) def encrypt_and_encode(self, data: dict) -> bytes: diff --git a/udp_bridge/receiver.py b/udp_bridge/receiver.py index 52b1b49..a0db80a 100755 --- a/udp_bridge/receiver.py +++ b/udp_bridge/receiver.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 import socket -from typing import Optional +from threading import Thread import rclpy from rclpy.node import Node + from udp_bridge.message_handler import MessageHandler -from threading import Thread class UdpBridgeReceiver: @@ -22,7 +22,7 @@ def __init__(self, node: Node): self.known_senders: list[str] = [] self.publishers = {} - encryption_key: Optional[str] = None + encryption_key: str | None = None if node.has_parameter("encryption_key"): encryption_key = node.get_parameter("encryption_key").value @@ -32,14 +32,14 @@ def recv_message(self): """ Receive a message from the network, process it and publish it into ROS """ - acc = bytes() + acc = b"" while rclpy.ok(): try: acc += self.sock.recv(10240) if acc[-3:] == MessageHandler.PACKAGE_DELIMITER: self.handle_message(acc[:-3]) - acc = bytes() + acc = b"" except socket.timeout: pass diff --git a/udp_bridge/sender.py b/udp_bridge/sender.py index a8c0ca9..d18812a 100755 --- a/udp_bridge/sender.py +++ b/udp_bridge/sender.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 -from queue import Empty, Full, Queue import socket -from typing import Optional +from queue import Empty, Full, Queue -from bitbots_utils.utils import get_parameters_from_other_node import rclpy +from bitbots_utils.utils import get_parameters_from_other_node from rclpy.executors import SingleThreadedExecutor from rclpy.node import Node from rclpy.subscription import Subscription from rclpy.timer import Timer from ros2topic.api import get_msg_class, get_topic_names + from udp_bridge.message_handler import MessageHandler HOSTNAME = socket.gethostname() @@ -33,9 +33,9 @@ def __init__(self, topic: str, queue_size: int, message_handler: MessageHandler, self.queue: Queue = Queue(queue_size) self.message_handler: MessageHandler = message_handler self.node: Node = node - self.timer: Optional[Timer] = None + self.timer: Timer | None = None - self.__subscriber: Optional[Subscription] = None + self.__subscriber: Subscription | None = None self.__subscribe() def __subscribe(self, backoff=1.0): @@ -145,7 +145,7 @@ def setup_udp_socket(self) -> socket.socket: return sock def setup_message_handler(self) -> MessageHandler: - encryption_key: Optional[str] = None + encryption_key: str | None = None if self.node.has_parameter("encryption_key"): encryption_key = self.node.get_parameter("encryption_key").value