diff --git a/rosdoc2/verbs/build/builders/sphinx_builder.py b/rosdoc2/verbs/build/builders/sphinx_builder.py
index 2dd2bd3..e57070a 100644
--- a/rosdoc2/verbs/build/builders/sphinx_builder.py
+++ b/rosdoc2/verbs/build/builders/sphinx_builder.py
@@ -385,24 +385,33 @@ def build(self, *, doc_build_folder, output_staging_directory):
package_xml_directory = os.path.dirname(self.build_context.package.filename)
# If 'python_source' is specified, construct 'python_src_directory' from it
+ python_src_directory = None
if self.build_context.python_source is not None:
python_src_directory = \
os.path.abspath(
os.path.join(
package_xml_directory,
self.build_context.python_source))
- # If not provided, try to find the python source directory
- else:
- package_list = setuptools.find_packages(where=package_xml_directory)
- if self.build_context.package.name in package_list:
- python_src_directory = \
- os.path.abspath(
- os.path.join(
- package_xml_directory,
- self.build_context.package.name))
- else:
+ if not os.path.isdir(python_src_directory):
+ logger.warning(f'python_source specified as {self.build_context.python_source} '
+ 'is not a directory')
python_src_directory = None
+ # If not provided or invalid, try to find the python source directory
+ if not python_src_directory:
+ search_dirs = ['.', 'src']
+ for search_dir in search_dirs:
+ where = os.path.abspath(os.path.join(package_xml_directory, search_dir))
+ package_list = setuptools.find_packages(where=where)
+ if self.build_context.package.name in package_list:
+ python_src_directory = \
+ os.path.abspath(
+ os.path.join(
+ where,
+ self.build_context.package.name))
+ if python_src_directory:
+ break
+
# We will ultimately run the sphinx project from a wrapped directory. Create it now,
# so that we can put generated items there.
wrapped_sphinx_directory = os.path.abspath(
@@ -493,25 +502,26 @@ def build(self, *, doc_build_folder, output_staging_directory):
# If the package has python code, then invoke `sphinx-apidoc` before building
if has_python:
if not python_src_directory or not os.path.isdir(python_src_directory):
- raise RuntimeError(
+ logger.warning(
'Could not locate source directory to invoke sphinx-apidoc in. '
'If this is package does not have a standard Python package layout, '
"please specify the Python source in 'rosdoc2.yaml'.")
- cmd = [
- 'sphinx-apidoc',
- '-o', wrapped_sphinx_directory,
- '-e', # Document each module in its own page.
- python_src_directory,
- ]
- logger.info(
- f"Running sphinx-apidoc: '{' '.join(cmd)}' in '{wrapped_sphinx_directory}'"
- )
- completed_process = subprocess.run(cmd, cwd=wrapped_sphinx_directory)
- msg = f"sphinx-apidoc exited with return code '{completed_process.returncode}'"
- if completed_process.returncode == 0:
- logger.debug(msg)
else:
- raise RuntimeError(msg)
+ cmd = [
+ 'sphinx-apidoc',
+ '-o', wrapped_sphinx_directory,
+ '-e', # Document each module in its own page.
+ python_src_directory,
+ ]
+ logger.info(
+ f"Running sphinx-apidoc: '{' '.join(cmd)}' in '{wrapped_sphinx_directory}'"
+ )
+ completed_process = subprocess.run(cmd, cwd=wrapped_sphinx_directory)
+ msg = f"sphinx-apidoc exited with return code '{completed_process.returncode}'"
+ if completed_process.returncode == 0:
+ logger.debug(msg)
+ else:
+ logger.warning(msg)
# Invoke Sphinx-build.
sphinx_output_dir = os.path.abspath(
diff --git a/rosdoc2/verbs/build/inspect_package_for_settings.py b/rosdoc2/verbs/build/inspect_package_for_settings.py
index 2ff3985..4fd5083 100644
--- a/rosdoc2/verbs/build/inspect_package_for_settings.py
+++ b/rosdoc2/verbs/build/inspect_package_for_settings.py
@@ -49,7 +49,7 @@
## be assumed for 'sphinx-apidoc' invocation. The user can provide the path
## (relative to the 'package.xml' file) where the Python modules defined by this
## package are located.
- python_source: '{package_name}'
+ # python_source: '{package_name}'
## This setting, if true, attempts to run `doxygen` and the `breathe`/`exhale`
## extensions to `sphinx` regardless of build type. This is most useful if the
diff --git a/test/packages/basic_cpp/package.xml b/test/packages/basic_cpp/package.xml
index 3c8165c..12e719a 100644
--- a/test/packages/basic_cpp/package.xml
+++ b/test/packages/basic_cpp/package.xml
@@ -14,5 +14,6 @@
ament_cmake
+ rosdoc2.yaml
diff --git a/test/packages/basic_cpp/rosdoc2.yaml b/test/packages/basic_cpp/rosdoc2.yaml
index 27248e8..92eda70 100644
--- a/test/packages/basic_cpp/rosdoc2.yaml
+++ b/test/packages/basic_cpp/rosdoc2.yaml
@@ -28,7 +28,7 @@ settings:
## be assumed for 'sphinx-apidoc' invocation. The user can provide the path
## (relative to the 'package.xml' file) where the Python modules defined by this
## package are located.
- python_source: 'basic_cpp'
+ # python_source: 'basic_cpp'
## This setting, if true, attempts to run `doxygen` and the `breathe`/`exhale`
## extensions to `sphinx` regardless of build type. This is most useful if the
diff --git a/test/packages/false_python/package.xml b/test/packages/false_python/package.xml
new file mode 100644
index 0000000..ef8fcbb
--- /dev/null
+++ b/test/packages/false_python/package.xml
@@ -0,0 +1,12 @@
+
+
+
+ false_python
+ 0.0.0
+ I say I am python, but no actual python
+ Ye ol' Python Pro
+ Apache-2.0
+
+ ament_python
+
+
diff --git a/test/packages/invalid_python_source/package.xml b/test/packages/invalid_python_source/package.xml
new file mode 100644
index 0000000..88f96d9
--- /dev/null
+++ b/test/packages/invalid_python_source/package.xml
@@ -0,0 +1,16 @@
+
+
+
+ invalid_python_source
+ 0.0.0
+ This packages incorrectly specifies python source
+ ros2 user
+ Apache 2.0
+
+ ament_cmake
+ ament_cmake_python
+
+ ament_cmake
+ rosdoc2.yaml
+
+
diff --git a/test/packages/invalid_python_source/rosdoc2.yaml b/test/packages/invalid_python_source/rosdoc2.yaml
new file mode 100644
index 0000000..24c7dd4
--- /dev/null
+++ b/test/packages/invalid_python_source/rosdoc2.yaml
@@ -0,0 +1,68 @@
+## Default configuration, generated by rosdoc2.
+
+## This 'attic section' self-documents this file's type and version.
+type: 'rosdoc2 config'
+version: 1
+
+---
+
+settings:
+ ## If this is true, a standard index page is generated in the output directory.
+ ## It uses the package information from the 'package.xml' to show details
+ ## about the package, creates a table of contents for the various builders
+ ## that were run, and may contain links to things like build farm jobs for
+ ## this package or links to other versions of this package.
+
+ ## If false, you can still include content that would have been in the index
+ ## into one of your '.rst' files from your Sphinx project, using the
+ ## '.. include::' directive in Sphinx.
+ ## For example, you could include it in a custom 'index.rst' so you can have
+ ## the standard information followed by custom content.
+
+ ## TODO(wjwwood): provide a concrete example of this (relative path?)
+
+ ## If this is not specified explicitly, it defaults to 'true'.
+ generate_package_index: true
+
+ ## This setting is relevant mostly if the standard Python package layout cannot
+ ## be assumed for 'sphinx-apidoc' invocation. The user can provide the path
+ ## (relative to the 'package.xml' file) where the Python modules defined by this
+ ## package are located.
+ python_source: 'i_do_not_exist'
+
+ ## This setting, if true, attempts to run `doxygen` and the `breathe`/`exhale`
+ ## extensions to `sphinx` regardless of build type. This is most useful if the
+ ## user would like to generate C/C++ API documentation for a package that is not
+ ## of the `ament_cmake/cmake` build type.
+ always_run_doxygen: false
+
+ ## This setting, if true, attempts to run `sphinx-apidoc` regardless of build
+ ## type. This is most useful if the user would like to generate Python API
+ ## documentation for a package that is not of the `ament_python` build type.
+ always_run_sphinx_apidoc: false
+
+ # This setting, if provided, will override the build_type of this package
+ # for documentation purposes only. If not provided, documentation will be
+ # generated assuming the build_type in package.xml.
+ # override_build_type: 'ament_python'
+builders:
+ ## Each stanza represents a separate build step, performed by a specific 'builder'.
+ ## The key of each stanza is the builder to use; this must be one of the
+ ## available builders.
+ ## The value of each stanza is a dictionary of settings for the builder that
+ ## outputs to that directory.
+ ## Required keys in the settings dictionary are:
+ ## * 'output_dir' - determines output subdirectory for builder instance
+ ## relative to --output-directory
+ ## * 'name' - used when referencing the built docs from the index.
+
+ - doxygen: {
+ name: 'basic_cpp Public C/C++ API',
+ output_dir: 'generated/doxygen'
+ }
+ - sphinx: {
+ name: 'basic_cpp',
+ ## This path is relative to output staging.
+ doxygen_xml_directory: 'generated/doxygen/xml',
+ output_dir: ''
+ }
diff --git a/test/packages/src_alt_python/launch/dummy.launch.py b/test/packages/src_alt_python/launch/dummy.launch.py
new file mode 100644
index 0000000..f77f4bd
--- /dev/null
+++ b/test/packages/src_alt_python/launch/dummy.launch.py
@@ -0,0 +1 @@
+print('This is a dummy launch file')
diff --git a/test/packages/src_alt_python/package.xml b/test/packages/src_alt_python/package.xml
new file mode 100644
index 0000000..8f761d0
--- /dev/null
+++ b/test/packages/src_alt_python/package.xml
@@ -0,0 +1,12 @@
+
+
+
+ src_alt_python
+ 0.0.0
+ Python with python module name different from ros name
+ Ye ol' Python Pro
+ Apache-2.0
+
+ ament_python
+
+
diff --git a/test/packages/src_alt_python/src/other_name/__init__.py b/test/packages/src_alt_python/src/other_name/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/packages/src_alt_python/src/other_name/python_node.py b/test/packages/src_alt_python/src/other_name/python_node.py
new file mode 100644
index 0000000..2d9cc7a
--- /dev/null
+++ b/test/packages/src_alt_python/src/other_name/python_node.py
@@ -0,0 +1,6 @@
+def main():
+ print('Hello, world.')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/packages/src_alt_python/src/other_name/subpackage/__init__.py b/test/packages/src_alt_python/src/other_name/subpackage/__init__.py
new file mode 100644
index 0000000..6d5477c
--- /dev/null
+++ b/test/packages/src_alt_python/src/other_name/subpackage/__init__.py
@@ -0,0 +1 @@
+# This package should not be the python source because it is a sub package
diff --git a/test/packages/src_python/package.xml b/test/packages/src_python/package.xml
new file mode 100644
index 0000000..302ee69
--- /dev/null
+++ b/test/packages/src_python/package.xml
@@ -0,0 +1,12 @@
+
+
+
+ src_python
+ 0.0.0
+ Basic Python node with source layout
+ Ye ol' Python Pro
+ Apache-2.0
+
+ ament_python
+
+
diff --git a/test/packages/src_python/src/src_python/__init__.py b/test/packages/src_python/src/src_python/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/packages/src_python/src/src_python/python_node.py b/test/packages/src_python/src/src_python/python_node.py
new file mode 100644
index 0000000..2d9cc7a
--- /dev/null
+++ b/test/packages/src_python/src/src_python/python_node.py
@@ -0,0 +1,6 @@
+def main():
+ print('Hello, world.')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/packages/too_many_python_packages/package.xml b/test/packages/too_many_python_packages/package.xml
new file mode 100644
index 0000000..96edab2
--- /dev/null
+++ b/test/packages/too_many_python_packages/package.xml
@@ -0,0 +1,15 @@
+
+
+
+ too_many_python_packages
+ 0.0.0
+ Too many unspecified python packages
+ ros2 user
+ Apache 2.0
+
+ ament_cmake
+ ament_cmake_python
+
+ ament_cmake
+
+
diff --git a/test/packages/too_many_python_packages/package1/__init__.py b/test/packages/too_many_python_packages/package1/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/packages/too_many_python_packages/package1/python_node.py b/test/packages/too_many_python_packages/package1/python_node.py
new file mode 100644
index 0000000..2d9cc7a
--- /dev/null
+++ b/test/packages/too_many_python_packages/package1/python_node.py
@@ -0,0 +1,6 @@
+def main():
+ print('Hello, world.')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/packages/too_many_python_packages/package2/__init__.py b/test/packages/too_many_python_packages/package2/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/packages/too_many_python_packages/package2/python_node.py b/test/packages/too_many_python_packages/package2/python_node.py
new file mode 100644
index 0000000..2d9cc7a
--- /dev/null
+++ b/test/packages/too_many_python_packages/package2/python_node.py
@@ -0,0 +1,6 @@
+def main():
+ print('Hello, world.')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/test_builder.py b/test/test_builder.py
index 522d234..9123b5e 100644
--- a/test/test_builder.py
+++ b/test/test_builder.py
@@ -263,6 +263,56 @@ def test_only_python(session_dir):
links_exist=links_exist)
+def test_src_python(session_dir):
+ PKG_NAME = 'src_python'
+ do_build_package(DATAPATH / PKG_NAME, session_dir)
+
+ includes = ['src_python package']
+ links_exist = ['src_python.html']
+
+ do_build_package(DATAPATH / PKG_NAME, session_dir)
+
+ do_test_package(PKG_NAME, session_dir,
+ includes=includes,
+ links_exist=links_exist)
+
+
+def test_false_python(session_dir):
+ PKG_NAME = 'false_python'
+ do_build_package(DATAPATH / PKG_NAME, session_dir)
+
+ excludes = ['python api']
+ includes = ['I say I am python, but no actual python']
+
+ do_test_package(PKG_NAME, session_dir,
+ includes=includes,
+ excludes=excludes)
+
+
+def test_invalid_python_source(session_dir):
+ PKG_NAME = 'invalid_python_source'
+ do_build_package(DATAPATH / PKG_NAME, session_dir)
+
+ excludes = ['python api']
+ includes = ['This packages incorrectly specifies python source']
+
+ do_test_package(PKG_NAME, session_dir,
+ includes=includes,
+ excludes=excludes)
+
+
+def test_too_many_python_packages(session_dir):
+ PKG_NAME = 'too_many_python_packages'
+ do_build_package(DATAPATH / PKG_NAME, session_dir)
+
+ excludes = ['python api']
+ includes = ['Too many unspecified python packages']
+
+ do_test_package(PKG_NAME, session_dir,
+ includes=includes,
+ excludes=excludes)
+
+
def test_only_messages(session_dir):
"""Test a package only containing messages."""
PKG_NAME = 'only_messages'