Skip to content

Commit

Permalink
Backport Python Actions (#32)
Browse files Browse the repository at this point in the history
* Adds Python typesupport for Actions (#21)

* Add _action.py.em; Makes rosidl_generator_py handle action files
properly

* Adds CMake pipeline bits for action generation.

* Fixes all linter issues.

* Attempts to fix CMake warnings in Windows.

* Fix flake8 error (#27)

* Provide type support for 'goal_status_array' in action type support

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Fix spelling typo

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Ignore import order on generated imports (#29)

Signed-off-by: Shane Loretz <sloretz@osrfoundation.org>
  • Loading branch information
jacobperron authored and nuclearsandwich committed Mar 10, 2019
1 parent 0f5c8f3 commit 8a7f4b3
Show file tree
Hide file tree
Showing 11 changed files with 421 additions and 25 deletions.
3 changes: 3 additions & 0 deletions python_cmake_module/cmake/Modules/FindPythonExtra.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@

set(PythonExtra_FOUND FALSE)

# Prevent find_package(PythonLibs) from getting confused.
unset(PYTHON_LIBRARY)

find_package(PythonInterp 3.5 REQUIRED)

if(PYTHONINTERP_FOUND)
Expand Down
2 changes: 1 addition & 1 deletion rosidl_generator_py/cmake/custom_command.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ add_custom_command(
COMMAND ${PYTHON_EXECUTABLE} ${rosidl_generator_py_BIN}
--generator-arguments-file "${generator_arguments_file}"
--typesupport-impls "${_typesupport_impls}"
DEPENDS ${target_dependencies} ${rosidl_generate_interfaces_TARGET}
DEPENDS ${target_dependencies} ${rosidl_generate_interfaces_TARGET} ${extra_generator_dependencies}
COMMENT "Generating Python code for ROS interfaces"
VERBATIM
)
Expand Down
5 changes: 5 additions & 0 deletions rosidl_generator_py/cmake/register_py.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ macro(rosidl_generator_py_extras BIN GENERATOR_FILES TEMPLATE_DIR)
"rosidl_generator_py"
"rosidl_generator_py_generate_interfaces.cmake")

ament_register_extension(
"rosidl_generate_action_interfaces"
"rosidl_generator_py"
"rosidl_generator_py_generate_action_interfaces.cmake")

normalize_path(BIN "${BIN}")
set(rosidl_generator_py_BIN "${BIN}")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

find_package(rmw_implementation_cmake REQUIRED)
find_package(rmw REQUIRED)
find_package(rosidl_generator_c REQUIRED)
find_package(rosidl_typesupport_c REQUIRED)
find_package(rosidl_typesupport_interface REQUIRED)

find_package(PythonInterp 3.5 REQUIRED)

find_package(python_cmake_module REQUIRED)
find_package(PythonExtra MODULE REQUIRED)

set(_typesupport_impls "rosidl_typesupport_c")

set(_output_path
"${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py/${PROJECT_NAME}")

set(_generated_action_py_files "")
foreach(_idl_file ${rosidl_generate_action_interfaces_IDL_FILES})
get_filename_component(_extension "${_idl_file}" EXT)
get_filename_component(_parent_folder "${_idl_file}" DIRECTORY)
get_filename_component(_parent_folder "${_parent_folder}" NAME)
if(_extension STREQUAL ".action")
set(_allowed_parent_folders "action")
if(NOT _parent_folder IN_LIST _allowed_parent_folders)
message(FATAL_ERROR "Interface file with unknown parent folder: ${_idl_file}")
endif()
else()
message(FATAL_ERROR "Interface file with unknown extension: ${_idl_file}")
endif()
get_filename_component(_msg_name "${_idl_file}" NAME_WE)
string_camel_case_to_lower_case_underscore("${_msg_name}" _module_name)
list(APPEND _generated_action_py_files
"${_output_path}/${_parent_folder}/_${_module_name}.py"
)
endforeach()

foreach(_typesupport_impl ${_typesupport_impls})
set(_generated_extension_${_typesupport_impl}_files "")
endforeach()
set(_generated_extension_files "")

if(NOT _generated_action_py_files STREQUAL "")
foreach(_typesupport_impl ${_typesupport_impls})
list(APPEND _generated_extension_${_typesupport_impl}_files "${_output_path}/_${PROJECT_NAME}_action_s.ep.${_typesupport_impl}.c")
list(APPEND _generated_extension_files "${_generated_extension_${_typesupport_impl}_files}")
endforeach()
list(GET _generated_action_py_files 0 _action_file)
get_filename_component(_parent_folder "${_action_file}" DIRECTORY)
list(APPEND _generated_action_py_files "${_parent_folder}/__init__.py")
endif()

set(_dependency_files "")
set(_dependencies "")
foreach(_pkg_name ${rosidl_generate_action_interfaces_DEPENDENCY_PACKAGE_NAMES})
foreach(_idl_file ${${_pkg_name}_INTERFACE_FILES})
get_filename_component(_extension "${_idl_file}" EXT)
if(_extension STREQUAL ".msg")
set(_abs_idl_file "${${_pkg_name}_DIR}/../${_idl_file}")
normalize_path(_abs_idl_file "${_abs_idl_file}")
list(APPEND _dependency_files "${_abs_idl_file}")
list(APPEND _dependencies "${_pkg_name}:${_abs_idl_file}")
endif()
endforeach()
endforeach()

set(target_dependencies
"${rosidl_generator_py_BIN}"
${rosidl_generator_py_GENERATOR_FILES}
"${rosidl_generator_py_TEMPLATE_DIR}/_action.py.em"
"${rosidl_generator_py_TEMPLATE_DIR}/_msg_pkg_typesupport_entry_point.c.em"
${rosidl_generate_action_interfaces_IDL_FILES}
${_dependency_files})
foreach(dep ${target_dependencies})
if(NOT EXISTS "${dep}")
get_property(is_generated SOURCE "${dep}" PROPERTY GENERATED)
if(NOT ${_is_generated})
message(FATAL_ERROR "Target dependency '${dep}' does not exist")
endif()
endif()
endforeach()

set(generator_arguments_file "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py__generate_actions__arguments.json")
rosidl_write_generator_arguments(
"${generator_arguments_file}"
PACKAGE_NAME "${PROJECT_NAME}"
ROS_INTERFACE_FILES "${rosidl_generate_action_interfaces_IDL_FILES}"
ROS_INTERFACE_DEPENDENCIES "${_dependencies}"
OUTPUT_DIR "${_output_path}"
TEMPLATE_DIR "${rosidl_generator_py_TEMPLATE_DIR}"
TARGET_DEPENDENCIES ${target_dependencies}
)
# make sure that actions generation run sequentially with messages/services generation
set(extra_generator_dependencies "${rosidl_generate_interfaces_TARGET}__py")

set(_target_suffix "__action_py")

# move custom command into a subdirectory to avoid multiple invocations on Windows
set(_subdir "${CMAKE_CURRENT_BINARY_DIR}/${rosidl_generate_interfaces_TARGET}${_target_suffix}")
file(MAKE_DIRECTORY "${_subdir}")
file(READ "${rosidl_generator_py_DIR}/custom_command.cmake" _custom_command)
file(WRITE "${_subdir}/CMakeLists.txt" "${_custom_command}")
add_subdirectory("${_subdir}" ${rosidl_generate_interfaces_TARGET}${_target_suffix})
set_property(
SOURCE
${_generated_extension_files} ${_generated_action_py_files}
PROPERTY GENERATED 1)

macro(set_properties _build_type)
set_target_properties(${_target_name} PROPERTIES
COMPILE_OPTIONS "${_extension_compile_flags}"
PREFIX ""
LIBRARY_OUTPUT_DIRECTORY${_build_type} ${_output_path}
RUNTIME_OUTPUT_DIRECTORY${_build_type} ${_output_path}
OUTPUT_NAME "${PROJECT_NAME}_action_s__${_typesupport_impl}${PythonExtra_EXTENSION_SUFFIX}"
SUFFIX "${PythonExtra_EXTENSION_EXTENSION}")
endmacro()

foreach(_typesupport_impl ${_typesupport_impls})
find_package(${_typesupport_impl} REQUIRED)

set(_pyext_suffix "__pyext")
set(_target_name "${PROJECT_NAME}_action__${_typesupport_impl}${_pyext_suffix}")

add_library(${_target_name} SHARED
${_generated_extension_${_typesupport_impl}_files}
)
add_dependencies(
${_target_name}
${rosidl_generate_interfaces_TARGET}${_target_suffix}
${rosidl_generate_interfaces_TARGET}__rosidl_typesupport_c
)

set(_extension_compile_flags "")
set(_PYTHON_EXECUTABLE ${PYTHON_EXECUTABLE})
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set(_extension_compile_flags -Wall -Wextra)
endif()
if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(PYTHON_EXECUTABLE ${PYTHON_EXECUTABLE_DEBUG})
endif()
set_properties("")
if(WIN32)
set_properties("_DEBUG")
set_properties("_MINSIZEREL")
set_properties("_RELEASE")
set_properties("_RELWITHDEBINFO")
endif()
target_link_libraries(
${_target_name}
${PythonExtra_LIBRARIES}
${rosidl_generate_interfaces_TARGET}__${_typesupport_impl}
${rosidl_generate_interfaces_TARGET}__${_typesupport_impl}__generate_actions
)

target_include_directories(${_target_name}
PUBLIC
${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_c
${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py
${PythonExtra_INCLUDE_DIRS}
)

rosidl_target_interfaces(${_target_name}
${rosidl_generate_interfaces_TARGET} rosidl_typesupport_c)

ament_target_dependencies(${_target_name}
"rosidl_generator_c"
"rosidl_typesupport_c"
"rosidl_typesupport_interface"
)
foreach(_pkg_name ${rosidl_generate_interfaces_DEPENDENCY_PACKAGE_NAMES})
ament_target_dependencies(${_target_name}
${_pkg_name}
)
endforeach()

add_dependencies(${_target_name}
${rosidl_generate_interfaces_TARGET}__${_typesupport_impl}
${rosidl_generate_interfaces_TARGET}__${_typesupport_impl}__generate_actions
)
ament_target_dependencies(${_target_name}
"rosidl_generator_c"
"rosidl_generator_py"
"${rosidl_generate_interfaces_TARGET}__rosidl_generator_c"
)
set(PYTHON_EXECUTABLE ${_PYTHON_EXECUTABLE})

if(NOT rosidl_generate_interfaces_SKIP_INSTALL)
install(TARGETS ${_target_name}
DESTINATION "${PYTHON_INSTALL_DIR}/${PROJECT_NAME}")
endif()
endforeach()

if(NOT rosidl_generate_action_interfaces_SKIP_INSTALL)
if(NOT _generated_action_py_files STREQUAL "")
list(GET _generated_action_py_files 0 _action_file)
get_filename_component(_action_package_dir "${_action_file}" DIRECTORY)
get_filename_component(_action_package_dir "${_action_package_dir}" NAME)
if(NOT _action_package_dir STREQUAL "")
install(FILES ${_generated_action_py_files}
DESTINATION "${PYTHON_INSTALL_DIR}/${PROJECT_NAME}/${_action_package_dir}"
)
endif()
endif()
endif()

if(BUILD_TESTING AND rosidl_generate_action_interfaces_ADD_LINTER_TESTS)
if(NOT _generated_action_py_files STREQUAL "")
find_package(ament_cmake_flake8 REQUIRED)
ament_flake8(
TESTNAME "flake8_rosidl_generated_py_generate_actions"
# the generated code might contain longer lines for templated types
MAX_LINE_LENGTH 999
"${_output_path}")

find_package(ament_cmake_pep257 REQUIRED)
ament_pep257(
TESTNAME "pep257_rosidl_generated_py_generate_actions"
"${_output_path}")

find_package(ament_cmake_uncrustify REQUIRED)
ament_uncrustify(
TESTNAME "uncrustify_rosidl_generated_py_generate_actions"
# the generated code might contain longer lines for templated types
MAX_LINE_LENGTH 0
"${_output_path}")
endif()
endif()
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ rosidl_write_generator_arguments(
TEMPLATE_DIR "${rosidl_generator_py_TEMPLATE_DIR}"
TARGET_DEPENDENCIES ${target_dependencies}
)

set(extra_generator_dependencies "")

if(NOT _generated_msg_py_files STREQUAL "")
list(GET _generated_msg_py_files 0 _msg_file)
Expand Down
62 changes: 62 additions & 0 deletions rosidl_generator_py/resource/_action.py.em
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# generated from rosidl_generator_py/resource/_action.py.em
# generated code does not contain a copyright notice

@#######################################################################
@# EmPy template for generating _<action>.py files
@#
@# Context:
@# - module_name
@# - package_name
@# - spec (rosidl_parser.ActionSpecification)
@# Parsed specification of the .action file
@# - convert_camel_case_to_lower_case_underscore (function)
@#######################################################################
@
import logging
import traceback


class Metaclass(type):
"""Metaclass of action '@(spec.action_name)'."""

_TYPE_SUPPORT = None

@@classmethod
def __import_type_support__(cls):
try:
from rosidl_generator_py import import_type_support
module = import_type_support('@(package_name)', '@(package_name)_action')
except ImportError:
logger = logging.getLogger('rosidl_generator_py.@(spec.action_name)')
logger.debug(
'Failed to import needed modules for type support:\n' + traceback.format_exc())
else:
cls._TYPE_SUPPORT = module.type_support_action__@(subfolder)_@(module_name)
@{
prefix = '_' + convert_camel_case_to_lower_case_underscore(spec.action_name) + '__'
suffixes = ['feedback', 'goal', 'result']
for field_name in [prefix + suffix for suffix in suffixes]:
print('%sfrom %s.%s import %s' % (' ' * 4 * 3, package_name, subfolder, field_name))
print('%sif %s.Metaclass._TYPE_SUPPORT is None:' % (' ' * 4 * 3, field_name))
print('%s%s.Metaclass.__import_type_support__()' % (' ' * 4 * 4, field_name))
print('%sfrom %s.%s import %s' % (' ' * 4 * 3, 'action_msgs', 'msg', '_goal_status_array'))
print('%sif %s.Metaclass._TYPE_SUPPORT is None:' % (' ' * 4 * 3, '_goal_status_array'))
print('%s%s.Metaclass.__import_type_support__()' % (' ' * 4 * 4, '_goal_status_array'))
print('%sfrom %s.%s import %s' % (' ' * 4 * 3, 'action_msgs', 'srv', '_cancel_goal'))
print('%sif %s.Metaclass._TYPE_SUPPORT is None:' % (' ' * 4 * 3, '_cancel_goal'))
print('%s%s.Metaclass.__import_type_support__()' % (' ' * 4 * 4, '_cancel_goal'))
}@


class @(spec.action_name)(metaclass=Metaclass):
from action_msgs.srv._cancel_goal import CancelGoal as CancelGoalService
from action_msgs.msg._goal_status_array import GoalStatusArray as GoalStatusMessage
from @(package_name).@(subfolder)._@convert_camel_case_to_lower_case_underscore(spec.action_name)__goal import @(spec.action_name)_Goal as GoalRequestService
from @(package_name).@(subfolder)._@convert_camel_case_to_lower_case_underscore(spec.action_name)__result import @(spec.action_name)_Result as GoalResultService
from @(package_name).@(subfolder)._@convert_camel_case_to_lower_case_underscore(spec.action_name)__feedback import @(spec.action_name)_Feedback as Feedback

Goal = GoalRequestService.Request
Result = GoalResultService.Response

def __init__(self):
raise NotImplementedError('Action classes can not be instantiated')
Loading

0 comments on commit 8a7f4b3

Please sign in to comment.