ros2 / rclpy

rclpy (ROS Client Library for Python)
Apache License 2.0
289 stars 224 forks source link

Make nodes, publishers, subscriptions, services, clients, action servers, action clients Python context managers #1280

Closed clalancette closed 2 months ago

clalancette commented 4 months ago

Let's say that you want to create an rclpy node with a publisher and a subscription. In order to do this entirely correctly today, you would have to do the following:

rclpy.init()
try:
  node = rclpy.create_node('minimal_action_server')
  try:
    publisher = node.create_publisher(String, 'chatter', 1)
    try:
      subscription = node.create_subscription(String, 'chatter', lambda msg: print(msg), 1)
      try:
        do_thing()
      finally:
        node.destroy_subscription(subscription)
    finally:
      node.destroy_publisher(publisher)
  finally:
    node.destroy_node()
finally:
  rclpy.try_shutdown()

That is, every time we successfully create an entity, we really need to put a try..finally block around it to clean up only those parts that we've initialized so far. This is very unwieldy, so we don't actually recommend it in our examples or documentation.

However, we should be able to do better. In particular, if all of the initialization, nodes, publishers, subscriptions, services, clients, action servers, and action clients were Python context managers (i.e. they all implemented __enter__ and __exit__), this could be cleaned up to something like:

with rclpy.init() as init:
  with rclpy.create_node('minimal_action_server') as node:
    with node.create_publisher(String, 'chatter', 1) as pub:
      with node.create_subscription(String, 'chatter', lambda msg: print(msg), 1) as subscription:
        do_thing()

Or even better, using a contextlib.ExitStack:

with ExitStack() as stack:
    stack.enter_context(rclpy.init())
    node = stack.enter_context(rclpy.create_node('minimal_action_server'))
    publisher = stack.enter_context(node.create_publisher(String, 'chatter', 1))
    subscription = stack.enter_context(node.create_subscription(String, 'chatter', lambda msg: print(msg), 1))
    do_thing()

This came out of discussion around https://github.com/ros2/examples/pull/379

clalancette commented 2 months ago

While there are still a few outstanding pull requests for this, this has largely been completed in Rolling. Thus I'm going to close this one out. Thanks in particular to @sloretz for all of the reviews in getting this in.