Import modules in package in ROS2
Asked Answered
P

3

8

I have created a package for ROS2 and I have added a Python repository I downloaded. The problem I am having is that in the original repository the modules from the own repo were imported directly while in mine I have to import them adding the ROS2 package name before the module, even though I am importing a module from the same repo, like:

import planner_pkg.SimpleOneTrailerSystem as SimpleOneTrailerSystem

while I would like:

import SimpleOneTrailerSystem

My ROS2 project structure is like:

ros2_ws
  src
    planner
      planner_pkg
        __init__.py
        SimpleOneTrailerSystem.py
        planner_node.py
        ...
      package.xml
      setup.py

package.xml

<?xml version="1.0"?>
<package format="2">
  <name>planner_pkg</name>
  <version>0.0.1</version>
  <description>This package contains algorithm for park planner</description>

  <maintainer email=""></maintainer>
  <license>Apache License 2.0</license>

  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>

  <!-- These test dependencies are optional
  Their purpose is to make sure that the code passes the linters -->
  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

setup.py:

from setuptools import setup

package_name = 'planner_pkg'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    author='',
    author_email='',
    maintainer='',
    maintainer_email='',
    keywords=['ROS'],
    classifiers=[
        'Intended Audience :: Developers',
        'License :: OSI Approved :: Apache Software License',
        'Programming Language :: Python',
        'Topic :: Software Development',
    ],
    description='Package containing examples of how to use the rclpy API.',
    license='Apache License, Version 2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'planner_node = planner_pkg.planner_node:main',
        ],
    },
)

Pharisaism answered 9/8, 2019 at 8:59 Comment(2)
Placing external dependencies in your package is not usually recommended. Could you use a tool such as pip to install your dependency for you?Anchie
What I mean by external dependency is a module I have developed in another package, not a module developed by another person. It is explained in my questionPharisaism
M
13

First, according to the Module Search Path docs, when you do import something, Python looks for that something in the following places:

  • From the built-in modules
  • sys.path, which is a list containing:
    • The directory of the input script
    • PYTHONPATH, which is an environment variable containing a list of directories
    • The installation-dependent default

Second, when you build your ROS2 Python package (by calling colcon build invoking the ament_python build type), your Python codes will be copied over to an install folder with a tree structure like this:

install
...
├── planner_pkg
│   ├── bin
│   │   └── planner_node
│   ├── lib
│   │   └── python3.6
│   │       └── site-packages
│   │           ├── planner_pkg
│   │           │   ├── __init__.py
│   │           │   ├── planner_node.py
│   │           │   └── SimpleOneTrailerSystem.py
...

Now, when you do import SimpleOneTrailerSystem, Python will first search for it from the built-in modules, which for sure it won't find there. Next on the list is from sys.path. You can add a print(sys.path) at the top of planner_node.py to see something like this list:

['/path/to/install/planner_pkg/bin', 
 '/path/to/install/planner_pkg/lib/python3.6/site-packages', 
 '/opt/ros/eloquent/lib/python3.6/site-packages', 
 '/usr/lib/python36.zip', 
 '/usr/lib/python3.6', 
 ...other Python3.6 installation-dependent dirs...
]

First on the sys.path list is the bin folder of the input script. There are only executables there, no SimpleOneTrailerSystem.py file/module, so that import will fail.

The next on the list would be the planner_pkg/lib/pythonX.X/site-packages, and as you can see from the tree structure above, there is a SimpleOneTrailerSystem.py module BUT it is under a planner_pkg folder. So a direct import like this

import SimpleOneTrailerSystem

will not work. You need to qualify it with the package folder like this:

import planner_pkg.SimpleOneTrailerSystem

There are 2 ways to get around this.

  1. Modify sys.path before import-ing SimpleOneTrailerSystem

    import sys
    sys.path.append("/path/to/install/planner_pkg/lib/python3.6/site-packages/planner_pkg")
    
    import SimpleOneTrailerSystem
    

    This approach adds the path to the planner_pkg install directory to the sys.path list so that you don't need to specify it in subsequent imports.

  2. Modify PYTHONPATH before running your ROS2 node

    $ colcon build
    $ source install/setup.bash
    $ export PYTHONPATH=$PYTHONPATH:/path/to/install/planner_pkg/lib/python3.6/site-packages/planner_pkg
    $ planner_node
    

    This approach is almost the same as the first one, but there is no code change involved (and no rebuilding involved), as you only need to modify the PYTHONPATH environment variable.

Moldavia answered 22/10, 2019 at 13:4 Comment(0)
I
3

I had the same problem and fixed it by modifying setup.py.

Add:

('lib/' + package_name, [package_name+'/SimpleOneTrailerSystem.py']),

to "data_files" list.

setup.py:

from setuptools import setup

package_name = 'planner_pkg'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        ('lib/' + package_name, [package_name+'/SimpleOneTrailerSystem.py']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    author='',
    author_email='',
    maintainer='',
    maintainer_email='',
    keywords=['ROS'],
    classifiers=[
        'Intended Audience :: Developers',
        'License :: OSI Approved :: Apache Software License',
        'Programming Language :: Python',
        'Topic :: Software Development',
    ],
    description='Package containing examples of how to use the rclpy API.',
    license='Apache License, Version 2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'planner_node = planner_pkg.planner_node:main',
        ],
    },
)
Inception answered 28/11, 2022 at 10:33 Comment(0)
P
-1

For CMAKE packages with cpp and python code you can add the following lines to your CMakeLists.txt. This will copy your python_pkg folder into your install environment to "lib/python{version}/site-packages" which is per default included in your python path.

# for python code
find_package(ament_cmake_python REQUIRED)
find_package(rclpy REQUIRED)

install(DIRECTORY
    ../path_to_python_pkg
    DESTINATION lib/python3.8/site-packages
)

install(PROGRAMS
    ${PROJECT_NAME}/python_node.py
    DESTINATION lib/${PROJECT_NAME}
)

ament_python_install_package(${PROJECT_NAME})

If you are using e.g. Dashing or Eloquent it is "python3.6", for Foxy or newer it is "python3.8"

Postmillennialism answered 14/3, 2022 at 17:55 Comment(1)
I don't think this is very helpful. OP is writing a Python-only package.Christenachristendom

© 2022 - 2024 — McMap. All rights reserved.