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'