Creating a Python Library (for ament_python)

Let us start, as already recommended in this tutorial, with a template by ros2 pkg create.

cd ~/ros2_tutorial_workspace/src
ros2 pkg create python_package_with_a_library \
--build-type ament_python \
--library-name sample_python_library

which outputs the forever beautiful wall of text we’re now used to, with a minor difference regarding the additional library template, as highlighted below.

going to create a new package
package name: python_package_with_a_library
destination directory: /home/murilo/git/ROS2_Tutorial/ros2_tutorial_workspace/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['murilo <murilomarinho@ieee.org>']
licenses: ['TODO: License declaration']
build type: ament_python
dependencies: []
library_name: sample_python_library
creating folder ./python_package_with_a_library
creating ./python_package_with_a_library/package.xml
creating source folder
creating folder ./python_package_with_a_library/python_package_with_a_library
creating ./python_package_with_a_library/setup.py
creating ./python_package_with_a_library/setup.cfg
creating folder ./python_package_with_a_library/resource
creating ./python_package_with_a_library/resource/python_package_with_a_library
creating ./python_package_with_a_library/python_package_with_a_library/__init__.py
creating folder ./python_package_with_a_library/test
creating ./python_package_with_a_library/test/test_copyright.py
creating ./python_package_with_a_library/test/test_flake8.py
creating ./python_package_with_a_library/test/test_pep257.py
creating folder ./python_package_with_a_library/python_package_with_a_library/sample_python_library
creating ./python_package_with_a_library/python_package_with_a_library/sample_python_library/__init__.py

[WARNING]: Unknown license 'TODO: License declaration'.  This has been set in the package.xml, but no LICENSE file has been created.
It is recommended to use one of the ament license identitifers:
Apache-2.0
BSL-1.0
BSD-2.0
BSD-2-Clause
BSD-3-Clause
GPL-3.0-only
LGPL-3.0-only
MIT
MIT-0

The folders/files, Mason, what do they mean?

The ROS2 package created from the template has a structure like so. In particular, we can see that python_package_with_a_library is repeated twice in a row. This is a common source of error, so don’t forget!

python_package_with_a_library
   └── python_package_with_a_library
      └── sample_python_library
         __init__.py
      __init__.py
   └── resource
      python_package_with_a_library
   └── test
   package.xml
   setup.cfg
   setup.py

We learned the meaning of most of those in the preamble, namely (Murilo’s) Python Best Practices. To quickly clarify a few things, see the table below.

ROS2 Python package folders/files explained

File/Directory

Meaning

python_package_with_a_library

The ROS2 package folder.

python_package_with_a_library/python_package_with_a_library

The Python package, as we saw in the preamble.

sample_python_library

The module corresponding to our sample library.

resource/python_package_with_a_library

A file for ROS2 to index this package correctly. See Resource file.

test

The folder contaning the tests, as we already saw in the preamble.

setup.cfg

Used by setup.py, see setup.cfg docs.

setup.py

The instructions to make the package installable, as we saw in the preamble.

Overview of the library

Hint

If you have created the bad habit of declaring all/too many things in your __init__.py file, take the hint and start breaking the definitions into different files and use the __init__.py just to export the relevant parts of your library.

For the sake of the example, let us create a library with a Python function and another one with a class. To guide our next steps, we first draw a quick overview of what our python_package_with_a_library will look like.

python_package_with_a_library
   └── python_package_with_a_library
      └── sample_python_library
         __init__.py
         _sample_class.py
         _sample_function.py
      __init__.py
   └── resource
   └── test

With respect to the highlighted files, we will

  1. Create the _sample_function.py.

  2. Create the _sample_class.py.

  3. Modify __init__.py to use the new function and class.

All other files and directories will remain as-is, in the way they were generated by ros2 pkg create.

Create the sample function

Create a new file with the following contents and name.

~/ros2_tutorial_workspace/src/python_package_with_a_library/python_package_with_a_library/sample_python_library/_sample_function.py

1def sample_function_for_square_of_sum(a: float, b: float) -> float:
2    """Returns the square of a sum (a + b)^2 = a^2 + 2ab + b^2"""
3    return a**2 + 2*a*b + b**2

The function has two parameters, a and b. For simplicity, we’re expecting arguments of type float and returning a float, but it could be any Python function.

Create the sample class

Create a new file with the following contents and name.

~/ros2_tutorial_workspace/src/python_package_with_a_library/python_package_with_a_library/sample_python_library/_sample_class.py

 1class SampleClass:
 2    """A sample class to check how they can be imported by other ROS2 packages."""
 3
 4    def __init__(self, name: str):
 5        self._name = name
 6
 7    def get_name(self) -> str:
 8        """
 9        Gets the name of this instance.
10        :return: This name.
11        """
12        return self._name

The class is quite simple with a private data member and a method to retrieve it.

Modify the __init__.py to export the symbols

With the necessary files created and properly organized, the last step is to import the function and the class. We modify proper __init__.py file with the following contents.

~/ros2_tutorial_workspace/src/python_package_with_a_library/python_package_with_a_library/sample_python_library/__init__.py

1from python_package_with_a_library.sample_python_library._sample_class import SampleClass
2from python_package_with_a_library.sample_python_library._sample_function import sample_function_for_square_of_sum

Modify the setup.py to export the packages

Warning

This step might be unnecessary after this fix.

Note

This is a one-size-fits-most solution, which might not work for certain Python package structures. As a generic solution, we will export all Python packages in the ROS2 package excluding the test directory. For more information on setuptools, see the official Python packaging docs.

~/ros2_tutorial_workspace/src/python_package_with_a_library/setup.py

 1from setuptools import setup, find_packages
 2
 3package_name = 'python_package_with_a_library'
 4
 5setup(
 6    name=package_name,
 7    version='0.0.0',
 8    packages=find_packages(exclude=['test']),
 9    data_files=[
10        ('share/ament_index/resource_index/packages',
11            ['resource/' + package_name]),
12        ('share/' + package_name, ['package.xml']),
13    ],
14    install_requires=['setuptools'],
15    zip_safe=True,
16    maintainer='murilo',
17    maintainer_email='murilomarinho@ieee.org',
18    description='TODO: Package description',
19    license='TODO: License declaration',
20    tests_require=['pytest'],
21    entry_points={
22        'console_scripts': [
23        ],
24    },
25)

Build and source

No surprise here, right?

cd ~/ros2_tutorial_workspace
colcon build
source install/setup.bash

Note

If you don’t remember why we’re building with these commands, see Always source after you build.

If it builds without any unexpected issues, we’re good to go!