Skip to content

Commit 73f4a45

Browse files
authored
Add python message generation (#362)
Signed-off-by: Addisu Z. Taddese <[email protected]>
1 parent 4372e74 commit 73f4a45

8 files changed

+169
-32
lines changed

.github/ci/after_make.sh

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# It's necessary to install the python modules for the test.
2+
make install

.github/ci/packages.apt

+2
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ libprotoc-dev
66
libtinyxml2-dev
77
protobuf-compiler
88
ruby
9+
python3-pytest
10+
python3-protobuf

CMakeLists.txt

+16
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ set(
5959
"gz_msgs_gen executable used in the gz_msgs_protoc CMake function.")
6060
mark_as_advanced(GZ_MSGS_GEN_EXECUTABLE)
6161

62+
# Python interfaces vars
63+
option(USE_SYSTEM_PATHS_FOR_PYTHON_INSTALLATION
64+
"Install python modules in standard system paths in the system"
65+
OFF)
66+
67+
option(USE_DIST_PACKAGES_FOR_PYTHON
68+
"Use dist-packages instead of site-package to install python modules"
69+
OFF)
70+
6271
#============================================================================
6372
# Search for project-specific dependencies
6473
#============================================================================
@@ -87,6 +96,10 @@ set(GZ_TOOLS_VER 1)
8796
# Find Tinyxml2
8897
gz_find_package(TINYXML2 REQUIRED PRIVATE PRETTY tinyxml2)
8998

99+
#--------------------------------------
100+
# Find Python
101+
find_package(Python3 REQUIRED COMPONENTS Interpreter)
102+
90103
#============================================================================
91104
# Configure the build
92105
#============================================================================
@@ -111,6 +124,9 @@ add_subdirectory(tools)
111124
# projects.
112125
add_subdirectory(proto)
113126

127+
# Generate python
128+
add_subdirectory(python)
129+
114130
#============================================================================
115131
# Create package information
116132
#============================================================================

python/CMakeLists.txt

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Append `_configured` to the file name so it doesn't interfere with tests.
2+
# This happens because pytest will load the `gz.msgs` package from the build directory
3+
# (because there's an __init__.py file there) instead of being redirected to
4+
# `gz.msgs10` in the install directory, which is the intent of this `__init__.py` file.
5+
set(python_init_file ${PROJECT_BINARY_DIR}/python/gz/${GS_DESIGNATION}/__init__.py_configured)
6+
configure_file(${PROJECT_SOURCE_DIR}/python/src/__init__.py.in ${python_init_file})
7+
8+
install(FILES ${python_init_file} DESTINATION ${CMAKE_INSTALL_PREFIX}/${GZ_LIB_INSTALL_DIR}/python/gz/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR} RENAME __init__.py)
9+
10+
if (BUILD_TESTING AND NOT WIN32)
11+
set(python_tests
12+
basic_TEST
13+
)
14+
execute_process(COMMAND "${Python3_EXECUTABLE}" -m pytest --version
15+
OUTPUT_VARIABLE PYTEST_output
16+
ERROR_VARIABLE PYTEST_error
17+
RESULT_VARIABLE PYTEST_result)
18+
if(${PYTEST_result} EQUAL 0)
19+
set(pytest_FOUND TRUE)
20+
else()
21+
message(WARNING "Pytest package not available: ${PYTEST_error}")
22+
message(WARNING "Output: ${PYTEST_output}")
23+
endif()
24+
25+
foreach (test ${python_tests})
26+
if (pytest_FOUND)
27+
add_test(NAME ${test}.py COMMAND
28+
"${Python3_EXECUTABLE}" -m pytest "${CMAKE_SOURCE_DIR}/python/test/${test}.py" --junitxml "${CMAKE_BINARY_DIR}/test_results/${test}.xml")
29+
else()
30+
add_test(NAME ${test}.py COMMAND
31+
"${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/python/test/${test}.py")
32+
endif()
33+
set(_env_vars "PYTHONPATH=${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/python/")
34+
set_tests_properties(${test}.py PROPERTIES ENVIRONMENT "${_env_vars}")
35+
endforeach()
36+
endif()

python/src/__init__.py.in

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (C) 2023 Open Source Robotics Foundation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License")
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# This file is a workaround for a limitation in out protobuf python generation
16+
# where a message that depends on another message will try to import the
17+
# corresponding python module using `gz.msgs` as the package name. However,
18+
# we're installing the python modules in a directory that contains the gz-msgs
19+
# major version number, so the import fails. This hack here overwrites the
20+
# entry for the unversioned module name in `sys.modules` to point to the
21+
# versioned module the first time a message module is loaded. Subsequent
22+
# imports with or without the major version number will work properly.
23+
24+
import sys
25+
26+
unversioned_module = "gz.msgs"
27+
versioned_module = "gz.msgs@PROJECT_VERSION_MAJOR@"
28+
if unversioned_module in sys.modules:
29+
print("Looks like you are combining different versions of {}. Found {} and"
30+
"{} This is not supported".format(sys.modules[unversioned_module],
31+
sys.modules[versioned_module]))
32+
else:
33+
sys.modules[unversioned_module] = sys.modules[versioned_module]

python/test/basic_TEST.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright (C) 2023 Open Source Robotics Foundation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License")
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from gz.msgs10.vector3d_pb2 import Vector3d
16+
17+
import unittest
18+
19+
20+
class BasicTest(unittest.TestCase):
21+
22+
def test_serialization(self):
23+
msg = Vector3d()
24+
msg.x = 1
25+
msg.y = 2
26+
msg.z = 3
27+
28+
serialized_msg = msg.SerializeToString()
29+
self.assertGreater(len(serialized_msg), 0)
30+
31+
msg_from_serialized = Vector3d()
32+
self.assertNotEqual(msg_from_serialized, msg)
33+
msg_from_serialized.ParseFromString(serialized_msg)
34+
self.assertEqual(msg_from_serialized, msg)
35+
36+
37+
if __name__ == '__main__':
38+
unittest.main()

src/CMakeLists.txt

+35-26
Original file line numberDiff line numberDiff line change
@@ -19,40 +19,38 @@ if(INSTALL_GZ_MSGS_GEN_EXECUTABLE)
1919
install(FILES $<TARGET_FILE:gz_msgs_gen> DESTINATION ${GZ_BIN_INSTALL_DIR} RENAME ign_msgs_gen PERMISSIONS OWNER_EXECUTE)
2020
endif()
2121

22-
find_package(Python3 REQUIRED COMPONENTS Interpreter)
23-
2422
##################################################
2523
# A function that calls protoc on a protobuf file
2624
# Options:
27-
# GENERATE_RUBY - generates ruby code for the message if specified
25+
# GENERATE_PYTHON - generates python code for the message if specified
2826
# GENERATE_CPP - generates c++ code for the message if specified
2927
# One value arguments:
3028
# PROTO_PACKAGE - Protobuf package the file belongs to (e.g. ".gz.msgs")
3129
# PROTOC_EXEC - Path to protoc
3230
# INPUT_PROTO - Path to the input .proto file
3331
# OUTPUT_CPP_DIR - Path where C++ files are saved
34-
# OUTPUT_RUBY_DIR - Path where Ruby files are saved
32+
# OUTPUT_PYTHON_DIR - Path where Python files are saved
3533
# OUTPUT_INCLUDES - A CMake variable name containing a list that the C++ header path should be appended to
3634
# OUTPUT_CPP_HH_VAR - A CMake variable name containing a list that the C++ header path should be appended to
3735
# OUTPUT_GZ_CPP_HH_VAR - A CMake variable name containing a list that the C++ header path should be appended to
3836
# OUTPUT_CPP_CC_VAR - A Cmake variable name containing a list that the C++ source path should be appended to
39-
# OUTPUT_RUBY_VAR - A Cmake variable name containing a list that the ruby file should be apenned to
37+
# OUTPUT_PYTHON_VAR - A Cmake variable name containing a list that the python file should be appended to
4038
# Multi value arguments
4139
# PROTO_PATH - Passed to protoc --proto_path
4240
function(gz_msgs_protoc)
43-
set(options GENERATE_RUBY GENERATE_CPP)
41+
set(options GENERATE_PYTHON GENERATE_CPP)
4442
set(oneValueArgs
4543
PROTO_PACKAGE
4644
PROTOC_EXEC
4745
INPUT_PROTO
4846
OUTPUT_CPP_DIR
49-
OUTPUT_RUBY_DIR
47+
OUTPUT_PYTHON_DIR
5048
OUTPUT_INCLUDES
5149
OUTPUT_CPP_HH_VAR
5250
OUTPUT_GZ_CPP_HH_VAR
5351
OUTPUT_DETAIL_CPP_HH_VAR
5452
OUTPUT_CPP_CC_VAR
55-
OUTPUT_RUBY_VAR)
53+
OUTPUT_PYTHON_VAR)
5654
set(multiValueArgs PROTO_PATH)
5755

5856
cmake_parse_arguments(gz_msgs_protoc "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
@@ -97,13 +95,13 @@ function(gz_msgs_protoc)
9795
set(${gz_msgs_protoc_OUTPUT_CPP_CC_VAR} ${${gz_msgs_protoc_OUTPUT_CPP_CC_VAR}} PARENT_SCOPE)
9896
endif()
9997

100-
if(gz_msgs_protoc_GENERATE_RUBY)
101-
file(MAKE_DIRECTORY ${gz_msgs_protoc_OUTPUT_RUBY_DIR})
102-
set(output_ruby "${gz_msgs_protoc_OUTPUT_RUBY_DIR}${proto_package_dir}/${FIL_WE}_pb.rb")
103-
list(APPEND ${gz_msgs_protoc_OUTPUT_RUBY_VAR} ${output_ruby})
104-
list(APPEND output_files ${output_ruby})
105-
list(APPEND protoc_args "--ruby_out=${gz_msgs_protoc_OUTPUT_RUBY_DIR}")
106-
set(${gz_msgs_protoc_OUTPUT_RUBY_VAR} ${${gz_msgs_protoc_OUTPUT_RUBY_VAR}} PARENT_SCOPE)
98+
if(gz_msgs_protoc_GENERATE_PYTHON)
99+
file(MAKE_DIRECTORY ${gz_msgs_protoc_OUTPUT_PYTHON_DIR})
100+
# Note: Both proto2 and proto3 use the _pb2.py suffix (https://protobuf.dev/reference/python/python-generated/#invocation)
101+
set(output_python "${gz_msgs_protoc_OUTPUT_PYTHON_DIR}${proto_package_dir}/${FIL_WE}_pb2.py")
102+
list(APPEND ${gz_msgs_protoc_OUTPUT_PYTHON_VAR} ${output_python})
103+
list(APPEND output_files ${output_python})
104+
set(${gz_msgs_protoc_OUTPUT_PYTHON_VAR} ${${gz_msgs_protoc_OUTPUT_PYTHON_VAR}} PARENT_SCOPE)
107105
endif()
108106

109107

@@ -120,10 +118,10 @@ function(gz_msgs_protoc)
120118
--output-cpp-path "${gz_msgs_protoc_OUTPUT_CPP_DIR}")
121119
endif()
122120

123-
if(${gz_msgs_protoc_GENERATE_RUBY})
121+
if(${gz_msgs_protoc_GENERATE_PYTHON})
124122
list(APPEND GENERATE_ARGS
125-
--generate-ruby
126-
--output-ruby-path "${gz_msgs_protoc_OUTPUT_RUBY_DIR}")
123+
--generate-python
124+
--output-python-path "${gz_msgs_protoc_OUTPUT_PYTHON_DIR}")
127125
endif()
128126

129127
add_custom_command(
@@ -151,15 +149,15 @@ foreach(proto_file ${proto_files})
151149
PROTO_PACKAGE
152150
.gz.msgs
153151
GENERATE_CPP
154-
GENERATE_RUBY
152+
GENERATE_PYTHON
155153
INPUT_PROTO
156154
${proto_file}
157155
PROTOC_EXEC
158156
protobuf::protoc
159157
OUTPUT_CPP_DIR
160158
"${PROJECT_BINARY_DIR}/include"
161-
OUTPUT_RUBY_DIR
162-
"${PROJECT_BINARY_DIR}/ruby"
159+
OUTPUT_PYTHON_DIR
160+
"${PROJECT_BINARY_DIR}/python"
163161
OUTPUT_INCLUDES
164162
gen_includes
165163
OUTPUT_CPP_HH_VAR
@@ -170,8 +168,8 @@ foreach(proto_file ${proto_files})
170168
gen_ign_headers
171169
OUTPUT_CPP_CC_VAR
172170
gen_sources
173-
OUTPUT_RUBY_VAR
174-
gen_ruby_scripts
171+
OUTPUT_PYTHON_VAR
172+
gen_python_scripts
175173
PROTO_PATH
176174
"${PROJECT_SOURCE_DIR}/proto")
177175
endforeach()
@@ -193,11 +191,22 @@ if(MSVC)
193191
add_definitions(/bigobj)
194192
endif()
195193

196-
set_source_files_properties(${gen_headers} ${gen_ign_headers} ${gen_detail_headers} ${gen_sources} ${gen_ruby_scripts}
194+
set_source_files_properties(${gen_headers} ${gen_ign_headers} ${gen_detail_headers} ${gen_sources} ${gen_python_scripts}
197195
PROPERTIES GENERATED TRUE)
198196

199-
message(STATUS "Installing Ruby messages to ${CMAKE_INSTALL_PREFIX}/${GZ_LIB_INSTALL_DIR}/ruby/gz/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}")
200-
install(FILES ${gen_ruby_scripts} DESTINATION ${CMAKE_INSTALL_PREFIX}/${GZ_LIB_INSTALL_DIR}/ruby/gz/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR})
197+
if(USE_SYSTEM_PATHS_FOR_PYTHON_INSTALLATION)
198+
if(USE_DIST_PACKAGES_FOR_PYTHON)
199+
string(REPLACE "site-packages" "dist-packages" GZ_PYTHON_INSTALL_PATH ${Python3_SITELIB})
200+
else()
201+
# Python3_SITELIB might use dist-packages in some platforms
202+
string(REPLACE "dist-packages" "site-packages" GZ_PYTHON_INSTALL_PATH ${Python3_SITELIB})
203+
endif()
204+
else()
205+
# If not a system installation, respect local paths
206+
set(GZ_PYTHON_INSTALL_PATH ${GZ_LIB_INSTALL_DIR}/python)
207+
endif()
208+
message(STATUS "Installing Python messages to ${GZ_PYTHON_INSTALL_PATH}/gz/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}")
209+
install(FILES ${gen_python_scripts} DESTINATION ${GZ_PYTHON_INSTALL_PATH}/gz/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR})
201210

202211
# Install gz/msgs
203212
gz_install_includes(

tools/gz_msgs_generate.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ def main(argv=sys.argv[1:]):
3535
help='Flag to indicate if C++ bindings should be generated',
3636
action='store_true')
3737
parser.add_argument(
38-
'--generate-ruby',
39-
help='Flag to indicate if Ruby bindings should be generated',
38+
'--generate-python',
39+
help='Flag to indicate if Python bindings should be generated',
4040
action='store_true')
4141
parser.add_argument(
4242
'--output-cpp-path',
4343
help='The basepath of the generated C++ files')
4444
parser.add_argument(
45-
'--output-ruby-path',
45+
'--output-python-path',
4646
help='The basepath of the generated C++ files')
4747
parser.add_argument(
4848
'--proto-path',
@@ -57,7 +57,7 @@ def main(argv=sys.argv[1:]):
5757
args = parser.parse_args(argv)
5858

5959
for input_file in args.input_path:
60-
# First generate the base cpp and ruby files
60+
# First generate the base cpp and python files
6161
cmd = [args.protoc_exec]
6262

6363
for pp in args.proto_path:
@@ -67,11 +67,12 @@ def main(argv=sys.argv[1:]):
6767
cmd += [f'--plugin=protoc-gen-ignmsgs={args.gz_generator_bin}']
6868
cmd += [f'--cpp_out=dllexport_decl=GZ_MSGS_VISIBLE:{args.output_cpp_path}']
6969
cmd += [f'--ignmsgs_out={args.output_cpp_path}']
70-
if args.generate_ruby:
71-
cmd += [f'--ruby_out=dllexport_decl=GZ_MSGS_VISIBLE:{args.output_ruby_path}']
70+
if args.generate_python:
71+
cmd += [f'--python_out={args.output_python_path}']
7272
cmd += [input_file]
7373

7474
try:
75+
print("cmd:", cmd)
7576
subprocess.check_call(cmd)
7677
except subprocess.CalledProcessError as e:
7778
print(f'Failed to execute protoc compiler: {e}')

0 commit comments

Comments
 (0)