357 lines
11 KiB
Python
357 lines
11 KiB
Python
# Copyright 2023 The Kubeflow Authors. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
import inspect
|
|
import os
|
|
import re
|
|
import sys
|
|
import textwrap
|
|
from typing import List
|
|
|
|
import commonmark
|
|
import docstring_parser
|
|
from google_cloud_pipeline_components import utils
|
|
from kfp import components
|
|
from kfp import dsl
|
|
import yaml
|
|
|
|
# setting this enables the .rst files to use the paths v1.bigquery.Component (etc.) rather than google_cloud_pipeline_components.v1.biquery.Component for shorter, readable representation in docs
|
|
gcpc_root_dir = os.path.abspath(
|
|
os.path.join(
|
|
os.path.dirname(__file__),
|
|
'..',
|
|
'..',
|
|
'google_cloud_pipeline_components',
|
|
)
|
|
)
|
|
|
|
# keep as append not insert, otherwise there is an issue with other package discovery
|
|
sys.path.append(gcpc_root_dir)
|
|
|
|
# preserve function docstrings for components by setting component decorators to passthrough decorators
|
|
# also enables autodoc to document the components as functions without using the autodata directive (https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-autodata)
|
|
def first_order_passthrough_decorator(func):
|
|
func._is_component = True
|
|
return func
|
|
|
|
|
|
def second_order_passthrough_decorator(*args, **kwargs):
|
|
|
|
def decorator(func):
|
|
func._is_component = True
|
|
return func
|
|
|
|
return decorator
|
|
|
|
|
|
def second_order_passthrough_decorator_for_pipeline(*args, **kwargs):
|
|
def decorator(func):
|
|
func._is_pipeline = True
|
|
return func
|
|
|
|
return decorator
|
|
|
|
|
|
def load_from_file(path: str):
|
|
with open(path) as f:
|
|
contents = f.read()
|
|
component_dict = yaml.safe_load(contents)
|
|
comp = components.load_component_from_text(contents)
|
|
description = component_dict.get('description', '')
|
|
comp.__doc__ = description
|
|
return comp
|
|
|
|
|
|
utils.gcpc_output_name_converter = second_order_passthrough_decorator
|
|
dsl.component = second_order_passthrough_decorator
|
|
dsl.container_component = first_order_passthrough_decorator
|
|
dsl.pipeline = second_order_passthrough_decorator_for_pipeline
|
|
components.load_component_from_file = load_from_file
|
|
|
|
|
|
class OutputPath(dsl.OutputPath):
|
|
|
|
def __repr__(self) -> str:
|
|
type_string = getattr(self.type, '__name__', '')
|
|
return f'dsl.OutputPath({type_string})'
|
|
|
|
|
|
dsl.OutputPath = OutputPath
|
|
|
|
|
|
class InputClass:
|
|
|
|
def __getitem__(self, type_) -> str:
|
|
type_string = getattr(type_, 'schema_title', getattr(type_, '__name__', ''))
|
|
return f'dsl.Input[{type_string}]'
|
|
|
|
|
|
Input = InputClass()
|
|
|
|
dsl.Input = Input
|
|
|
|
|
|
class OutputClass:
|
|
|
|
def __getitem__(self, type_) -> str:
|
|
type_string = getattr(type_, 'schema_title', getattr(type_, '__name__', ''))
|
|
return f'dsl.Output[{type_string}]'
|
|
|
|
|
|
Output = OutputClass()
|
|
|
|
dsl.Output = Output
|
|
|
|
# -- General configuration ---------------------------------------------------
|
|
|
|
# If your documentation needs a minimal Sphinx version, state it here.
|
|
# needs_sphinx = '1.0'
|
|
|
|
# Add any Sphinx extension module names here, as strings. They can be
|
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
# ones.
|
|
extensions = [
|
|
'sphinx.ext.autodoc',
|
|
'sphinx.ext.viewcode',
|
|
'sphinx.ext.napoleon',
|
|
'm2r2',
|
|
'sphinx_immaterial',
|
|
'autodocsumm',
|
|
'notfound.extension',
|
|
]
|
|
autodoc_default_options = {
|
|
'members': True,
|
|
'member-order': 'alphabetical',
|
|
'imported-members': True,
|
|
'undoc-members': True,
|
|
'show-inheritance': False,
|
|
'inherited-members': False,
|
|
'autosummary': False,
|
|
}
|
|
|
|
# notfound.extension: https://sphinx-notfound-page.readthedocs.io/en/latest/configuration.html#confval-notfound_context
|
|
notfound_context = {
|
|
'title': 'Page not found',
|
|
'body': textwrap.dedent("""
|
|
<head>
|
|
<title>Page not found</title>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>404: Page not found</h1>
|
|
<p>
|
|
It's likely the object or page you're looking for doesn't exist in this version of Google Cloud Pipeline Components. Please ensure you have the correct version selected.
|
|
</p>
|
|
<a href="https://google-cloud-pipeline-components.readthedocs.io/">Back to homepage</a>
|
|
</div>
|
|
</body>
|
|
"""),
|
|
}
|
|
|
|
html_theme = 'sphinx_immaterial'
|
|
html_title = 'Google Cloud Pipeline Components Reference Documentation'
|
|
html_static_path = ['_static']
|
|
html_css_files = ['custom.css']
|
|
html_theme_options = {
|
|
'icon': {
|
|
'repo': 'fontawesome/brands/github',
|
|
},
|
|
'repo_url': 'https://github.com/kubeflow/pipelines/tree/master/components/google-cloud',
|
|
'repo_name': 'pipelines',
|
|
'repo_type': 'github',
|
|
'edit_uri': 'https://github.com/kubeflow/pipelines/tree/master/components/google-cloud/docs/source',
|
|
'globaltoc_collapse': True,
|
|
'features': [
|
|
'navigation.expand',
|
|
# "navigation.tabs",
|
|
# "toc.integrate",
|
|
'navigation.sections',
|
|
# "navigation.instant",
|
|
# "header.autohide",
|
|
'navigation.top',
|
|
# "navigation.tracking",
|
|
'search.highlight',
|
|
'search.share',
|
|
'toc.follow',
|
|
'toc.sticky',
|
|
],
|
|
'palette': [{
|
|
'media': '(prefers-color-scheme: light)',
|
|
'scheme': 'default',
|
|
'primary': 'googleblue',
|
|
}],
|
|
'font': {'text': 'Open Sans'},
|
|
'version_dropdown': True,
|
|
'version_json': 'https://raw.githubusercontent.com/kubeflow/pipelines/master/components/google-cloud/docs/source/versions.json',
|
|
# "toc_title_is_page_title": True,
|
|
}
|
|
# Add any paths that contain templates here, relative to this directory.
|
|
templates_path = ['_templates']
|
|
|
|
# The suffix(es) of source filenames.
|
|
# You can specify multiple suffix as a list of string:
|
|
source_suffix = '.rst'
|
|
|
|
# The master toctree document.
|
|
master_doc = 'index'
|
|
|
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
# for a list of supported languages.
|
|
#
|
|
# This is also used if you do content translation via gettext catalogs.
|
|
# Usually you set "language" from the command line for these cases.
|
|
language = 'en'
|
|
|
|
# List of patterns, relative to source directory, that match files and
|
|
# directories to ignore when looking for source files.
|
|
# This pattern also affects html_static_path and html_extra_path.
|
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
|
|
# The name of the Pygments (syntax highlighting) style to use.
|
|
pygments_style = None
|
|
|
|
# The default sidebars (for documents that don't match any pattern) are
|
|
# defined by theme itself. Builtin themes are using these templates by
|
|
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
|
# 'searchbox.html']``.
|
|
#
|
|
# html_sidebars = {}
|
|
|
|
# -- Options for HTMLHelp output ---------------------------------------------
|
|
|
|
# Output file base name for HTML help builder.
|
|
htmlhelp_basename = 'GoogleCloudPipelineComponentsDocs'
|
|
|
|
|
|
def component_grouper(app, what, name, obj, section, parent):
|
|
if getattr(obj, '_is_component', False):
|
|
return 'Components'
|
|
|
|
|
|
def pipeline_grouper(app, what, name, obj, section, parent):
|
|
if getattr(obj, '_is_pipeline', False):
|
|
return 'Pipelines'
|
|
|
|
|
|
def autodoc_skip_member(app, what, name, obj, skip, options):
|
|
skip = True
|
|
if name == 'create_custom_training_job_op_from_component':
|
|
return skip
|
|
|
|
|
|
def make_docstring_lines_for_param(
|
|
param_name: str,
|
|
type_string: str,
|
|
description: str,
|
|
) -> List[str]:
|
|
WHITESPACE = ' '
|
|
|
|
return [
|
|
f'{WHITESPACE * 2}``{param_name}: {type_string}``',
|
|
f'{WHITESPACE * 4}{description}',
|
|
]
|
|
|
|
|
|
def get_return_section(component) -> List[str]:
|
|
"""Modifies docstring so that a return section can be treated as an
|
|
|
|
args section, then parses the docstring.
|
|
"""
|
|
docstring = inspect.getdoc(component)
|
|
type_hints = component.__annotations__
|
|
if docstring is None:
|
|
return []
|
|
|
|
# Returns and Return are the only two keywords docstring_parser uses for returns
|
|
# use newline to avoid replacements that aren't in the return section header
|
|
return_keywords = ['Returns:\n', 'Returns\n', 'Return:\n', 'Return\n']
|
|
for keyword in return_keywords:
|
|
if keyword in docstring:
|
|
modified_docstring = docstring.replace(keyword.strip(), 'Args:')
|
|
returns_docstring = docstring_parser.parse(modified_docstring)
|
|
new_docstring_lines = []
|
|
for param in returns_docstring.params:
|
|
type_string = (
|
|
repr(
|
|
type_hints.get(
|
|
param.arg_name,
|
|
type_hints.get(
|
|
param.arg_name,
|
|
'Unknown',
|
|
),
|
|
)
|
|
)
|
|
.lstrip("'")
|
|
.rstrip("'")
|
|
)
|
|
new_docstring_lines.extend(
|
|
make_docstring_lines_for_param(
|
|
param_name=param.arg_name.lstrip(
|
|
utils.DOCS_INTEGRATED_OUTPUT_RENAMING_PREFIX
|
|
),
|
|
type_string=type_string,
|
|
description=param.description,
|
|
)
|
|
)
|
|
return new_docstring_lines
|
|
return []
|
|
|
|
|
|
def remove__output_prefix_from_signature(
|
|
app, what, name, obj, options, signature, return_annotation
|
|
):
|
|
if signature is not None:
|
|
signature = re.sub(
|
|
rf'{utils.DOCS_INTEGRATED_OUTPUT_RENAMING_PREFIX}(\w+):',
|
|
r'\1:',
|
|
signature,
|
|
)
|
|
return signature, return_annotation
|
|
|
|
|
|
def remove_after_returns_in_place(lines: List[str]) -> bool:
|
|
for i in range(len(lines)):
|
|
if lines[i].startswith(':returns:'):
|
|
del lines[i:]
|
|
return True
|
|
return False
|
|
|
|
def process_named_docstring_returns(app, what, name, obj, options, lines):
|
|
markdown_to_rst(lines)
|
|
if getattr(obj, '_is_component', False):
|
|
has_returns_section = remove_after_returns_in_place(lines)
|
|
if has_returns_section:
|
|
returns_section = get_return_section(obj)
|
|
lines.extend([':returns:', ''])
|
|
lines.extend(returns_section)
|
|
|
|
|
|
def markdown_to_rst(lines: List[str]) -> List[str]:
|
|
md = '\n'.join(lines)
|
|
ast = commonmark.Parser().parse(md)
|
|
rst = commonmark.ReStructuredTextRenderer().render(ast)
|
|
lines.clear()
|
|
lines += rst.splitlines()
|
|
|
|
|
|
def setup(app):
|
|
app.connect('autodoc-process-docstring', process_named_docstring_returns)
|
|
app.connect(
|
|
'autodoc-process-signature',
|
|
remove__output_prefix_from_signature,
|
|
)
|
|
app.connect('autodocsumm-grouper', component_grouper)
|
|
app.connect('autodocsumm-grouper', pipeline_grouper)
|
|
app.connect('autodoc-skip-member', autodoc_skip_member)
|