From 4a77291bf4471852649a5fe2a731282af0e07c7e Mon Sep 17 00:00:00 2001 From: ooooo <3164076421@qq.com> Date: Mon, 6 Oct 2025 21:28:26 +0800 Subject: [PATCH 1/5] [CI] Add Report Preview URLs Workflow --- .github/workflows/Preview-Url-Comment.yml | 58 +++++++++++ .github/workflows/_Doc-Preview.yml | 38 ++++++- python/paddle/tensor/manipulation.py | 2 +- tools/generate_doc_comment.py | 116 ++++++++++++++++++++++ 4 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/Preview-Url-Comment.yml create mode 100644 tools/generate_doc_comment.py diff --git a/.github/workflows/Preview-Url-Comment.yml b/.github/workflows/Preview-Url-Comment.yml new file mode 100644 index 00000000000000..e2d69967db68df --- /dev/null +++ b/.github/workflows/Preview-Url-Comment.yml @@ -0,0 +1,58 @@ +name: Comment Preview URLs + +on: + workflow_run: + workflows: ["Doc-Preview"] + types: + - completed + +jobs: + comment: + name: Post Preview URLs Comment + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + permissions: + pull-requests: write + + steps: + - name: Download artifacts + id: download + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: doc-preview-comment + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Read artifacts + id: artifacts-data + if: steps.download.outcome == 'success' + run: | + PR_NUMBER=$(cat pr_number.txt) + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + COMMENT_BODY=$(cat comment_body.txt) + { + echo 'comment_body<> $GITHUB_OUTPUT + + - name: Find existing comment + id: fc + if: steps.download.outcome == 'success' + uses: peter-evans/find-comment@v4 + with: + issue-number: ${{ steps.artifacts-data.outputs.pr_number }} + comment-author: 'github-actions[bot]' + body-includes: 'Preview documentation links for API changes in this PR' + + - name: Create or update comment + if: steps.download.outcome == 'success' + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ steps.artifacts-data.outputs.pr_number }} + body: ${{ steps.artifacts-data.outputs.comment_body }} + edit-mode: replace diff --git a/.github/workflows/_Doc-Preview.yml b/.github/workflows/_Doc-Preview.yml index 04c3d77179c488..e26fea7aded1be 100644 --- a/.github/workflows/_Doc-Preview.yml +++ b/.github/workflows/_Doc-Preview.yml @@ -94,11 +94,13 @@ jobs: echo "Extracting build.tar.gz" git config --global --add safe.directory ${work_dir} tar --use-compress-program="pzstd -1" -xpf build.tar.gz --strip-components=1 - api_doc_spec_diff=$(python tools/diff_api.py paddle/fluid/API_DEV.spec.doc paddle/fluid/API_PR.spec.doc) - if [ "$api_doc_spec_diff" == "" ]; then + api_doc_spec_diff=$(python tools/diff_api.py paddle/fluid/API_DEV.spec.doc paddle/fluid/API_PR.spec.doc || true) + if [ -z "$api_doc_spec_diff" ]; then echo "API documents no change." exit 0 fi + # Save diff to a file for the next step + echo "$api_doc_spec_diff" > /tmp/api_doc_diff.txt curl -sS -o /tmp/entrypoint.sh https://paddle-dev-tools-open.bj.bcebos.com/fluiddoc-preview/entrypoint-paddle-docs-review.sh cd / @@ -106,6 +108,38 @@ jobs: bash "/tmp/entrypoint.sh" ' + - name: Generate Comment Body + id: generate_comment + run: | + comment_body=$(docker exec -t ${{ env.container_name }} /bin/bash -c ' + if [ ! -f "/tmp/api_doc_diff.txt" ]; then + exit 0 + fi + python /paddle/tools/generate_doc_comment.py /tmp/api_doc_diff.txt ${{ env.PR_ID }} + ') + echo "comment_body<> $GITHUB_OUTPUT + echo "$comment_body" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Save comment artifacts + if: steps.generate_comment.outputs.comment_body != '' + run: | + echo "${{ steps.generate_comment.outputs.comment_body }}" > comment_body.txt + echo "${{ env.PR_ID }}" > pr_number.txt + # [TODO] remove it after test + echo "${{ steps.generate_comment.outputs.comment_body }}" + echo "${{ env.PR_ID }}" + + - name: Upload comment artifacts + if: steps.generate_comment.outputs.comment_body != '' + uses: actions/upload-artifact@v4 + with: + name: doc-preview-comment + path: | + comment_body.txt + pr_number.txt + retention-days: 1 + - name: Terminate and delete the container if: always() run: | diff --git a/python/paddle/tensor/manipulation.py b/python/paddle/tensor/manipulation.py index 94a53b0ff6c920..4d0c454b357790 100644 --- a/python/paddle/tensor/manipulation.py +++ b/python/paddle/tensor/manipulation.py @@ -6584,7 +6584,7 @@ def as_complex(x: Tensor, name: str | None = None) -> Tensor: def as_real(x: Tensor, name: str | None = None) -> Tensor: - """Transform a complex tensor to a real tensor. + """Transform a complex tensor to a real tensor 1433223. The data type of the input tensor is 'complex64' or 'complex128', and the data type of the returned tensor is 'float32' or 'float64', respectively. diff --git a/tools/generate_doc_comment.py b/tools/generate_doc_comment.py new file mode 100644 index 00000000000000..ac2f446c029911 --- /dev/null +++ b/tools/generate_doc_comment.py @@ -0,0 +1,116 @@ +# Copyright (c) 2025 PaddlePaddle 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 importlib +import inspect +import re +import sys + +import paddle # noqa: F401 + + +def resolve_string_to_obj(path: str): + """ + Recursively resolves a string path to a Python object. + """ + if not path: + return None + + # First, try to import the entire path as a module (e.g., "paddle" or "paddle.autograd"). + try: + return importlib.import_module(path) + except ImportError: + # If the import fails, it might be an object within a module. + # If there's no dot, it was a failed top-level import, so we can't proceed. + if "." not in path: + return None + + # Split the path into its parent and the final object name. + # e.g., "paddle.Tensor" -> parent="paddle", child="Tensor" + parent_path, child_name = path.rsplit('.', 1) + parent_obj = resolve_string_to_obj(parent_path) + + # If the parent object could not be resolved, we can't find the child. + if parent_obj is None: + return None + + # Use getattr with a default value to safely get the child object. + return getattr(parent_obj, child_name, None) + + +def generate_comment_body(doc_diff, pr_id): + if not doc_diff: + return "" + + output_lines = [] + base_url = f"http://preview-paddle-pr-{pr_id}.paddle-docs-preview.paddlepaddle.org.cn/documentation/docs/en/api" + + # Extract API names like 'paddle.autograd.backward' from lines like: + # - paddle.autograd.backward (ArgSpec(...), ('document', ...)) + # + paddle.autograd.backward (ArgSpec(...), ('document', ...)) + apis = sorted( + set(re.findall(r"^[+]\s*([a-zA-Z0-9_.]+)\s*\(", doc_diff, re.MULTILINE)) + ) + + for api in apis: + api_obj = resolve_string_to_obj(api) + + if api_obj is None: + raise ValueError(f"Could not resolve API path: {api}") + + api_path = api.replace('.', '/') + url = f"{base_url}/{api_path}_en.html" + + if "." in api: + parent_path, child_name = api.rsplit('.', 1) + parent_obj = resolve_string_to_obj(parent_path) + if inspect.isclass(parent_obj) and inspect.isfunction(api_obj): + parent_api_path = parent_path.replace('.', '/') + url = f"{base_url}/{parent_api_path}_en.html#{child_name}" + + output_lines.append(f"- **{api}**: [Preview]({url})") + + if not output_lines: + return "" + + comment_body = """> [!NOTE] +> Please wait for the **Doc-Preview** workflow to complete before clicking the preview links below, otherwise you may see outdated content. + +
+📚 Preview documentation links for API changes in this PR (Click to expand) + +The following are preview links for new or modified API documentation in this PR: + +{} + +
""".format("\n".join(output_lines)) + + return comment_body + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print( + "Usage: python generate_doc_comment.py " + ) + sys.exit(1) + + doc_diff_path = sys.argv[1] + pr_id = sys.argv[2] + + with open(doc_diff_path, 'r') as f: + doc_diff_content = f.read() + + comment = generate_comment_body(doc_diff_content, pr_id) + print(comment) From 8ba25f22662c9bbc00fc5c6f8cbc1e6e7718465d Mon Sep 17 00:00:00 2001 From: ooooo <3164076421@qq.com> Date: Fri, 10 Oct 2025 08:16:12 +0800 Subject: [PATCH 2/5] revert api docstring change --- python/paddle/tensor/manipulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/paddle/tensor/manipulation.py b/python/paddle/tensor/manipulation.py index 4d0c454b357790..94a53b0ff6c920 100644 --- a/python/paddle/tensor/manipulation.py +++ b/python/paddle/tensor/manipulation.py @@ -6584,7 +6584,7 @@ def as_complex(x: Tensor, name: str | None = None) -> Tensor: def as_real(x: Tensor, name: str | None = None) -> Tensor: - """Transform a complex tensor to a real tensor 1433223. + """Transform a complex tensor to a real tensor. The data type of the input tensor is 'complex64' or 'complex128', and the data type of the returned tensor is 'float32' or 'float64', respectively. From f0e3d61ff1a5ad05fd1b8e41f5100471a61eb5dd Mon Sep 17 00:00:00 2001 From: ooooo <3164076421@qq.com> Date: Fri, 10 Oct 2025 13:32:29 +0800 Subject: [PATCH 3/5] revert debug code --- .github/workflows/_Doc-Preview.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/_Doc-Preview.yml b/.github/workflows/_Doc-Preview.yml index e26fea7aded1be..642bb1f87da80c 100644 --- a/.github/workflows/_Doc-Preview.yml +++ b/.github/workflows/_Doc-Preview.yml @@ -111,7 +111,7 @@ jobs: - name: Generate Comment Body id: generate_comment run: | - comment_body=$(docker exec -t ${{ env.container_name }} /bin/bash -c ' + comment_body=$(docker exec ${{ env.container_name }} /bin/bash -c ' if [ ! -f "/tmp/api_doc_diff.txt" ]; then exit 0 fi @@ -121,14 +121,19 @@ jobs: echo "$comment_body" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT + if [ -n "$comment_body" ]; then + echo "::group::📝 Generated Comment Preview" + echo "$comment_body" + echo "::endgroup::" + else + echo "::notice::No comment generated" + fi + - name: Save comment artifacts if: steps.generate_comment.outputs.comment_body != '' run: | echo "${{ steps.generate_comment.outputs.comment_body }}" > comment_body.txt echo "${{ env.PR_ID }}" > pr_number.txt - # [TODO] remove it after test - echo "${{ steps.generate_comment.outputs.comment_body }}" - echo "${{ env.PR_ID }}" - name: Upload comment artifacts if: steps.generate_comment.outputs.comment_body != '' From d26fc9f664f18e2becb2ff6a60106d8c780b3455 Mon Sep 17 00:00:00 2001 From: ooooo <3164076421@qq.com> Date: Fri, 10 Oct 2025 16:43:03 +0800 Subject: [PATCH 4/5] move note inside \details\ --- python/paddle/nn/functional/conv.py | 5 ++++- tools/generate_doc_comment.py | 14 ++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/python/paddle/nn/functional/conv.py b/python/paddle/nn/functional/conv.py index e9486e9647f789..dcdf924b881ca2 100644 --- a/python/paddle/nn/functional/conv.py +++ b/python/paddle/nn/functional/conv.py @@ -361,12 +361,15 @@ def conv1d( bias (Tensor, optional): The bias with shape [M,]. Default: None. stride (int|list|tuple, optional): The stride size. If stride is a list/tuple, it must contain one integers, (stride_size). Default: 1. - padding (int|str|tuple|list, optional): The padding size. Padding could be in one of the following forms. + padding (int|str|tuple|list, optional): The padding size. + Padding could be in one of the following forms. + 1. a string in ['valid', 'same']. 2. an int, which means the feature map is zero paded by size of `padding` on both sides. 3. a list[int] or tuple[int] whose length is 1, which means the feature map is zero paded by size of `padding[0]` on both sides. 4. a list[int] or tuple[int] whose length is 2. It has the form [pad_before, pad_after]. 5. a list or tuple of pairs of ints. It has the form [[pad_before, pad_after], [pad_before, pad_after], ...]. Note that, the batch dimension and channel dimension are also included. Each pair of integers correspond to the amount of padding for a dimension of the input. Padding in batch dimension and channel dimension should be [0, 0] or (0, 0). + The default value is 0. dilation (int|list|tuple, optional): The dilation size. If dilation is a list/tuple, it must contain one integer, (dilation_size). Default: 1. diff --git a/tools/generate_doc_comment.py b/tools/generate_doc_comment.py index ac2f446c029911..e6f4e209606268 100644 --- a/tools/generate_doc_comment.py +++ b/tools/generate_doc_comment.py @@ -84,12 +84,18 @@ def generate_comment_body(doc_diff, pr_id): if not output_lines: return "" - comment_body = """> [!NOTE] -> Please wait for the **Doc-Preview** workflow to complete before clicking the preview links below, otherwise you may see outdated content. - -
+ comment_body = """
📚 Preview documentation links for API changes in this PR (Click to expand) + + + + +
+â„šī¸ Preview Notice
+Please wait for the Doc-Preview workflow to complete before clicking the preview links below, otherwise you may see outdated content. +
+ The following are preview links for new or modified API documentation in this PR: {} From 9282515df52a68c6d5f5b81c6b575fd22d9f36ad Mon Sep 17 00:00:00 2001 From: ooooo <3164076421@qq.com> Date: Sun, 12 Oct 2025 19:21:57 +0800 Subject: [PATCH 5/5] use argparse to parse args && change error to comment_body && fix the judgment logic of Class.xxxx --- tools/generate_doc_comment.py | 75 ++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/tools/generate_doc_comment.py b/tools/generate_doc_comment.py index e6f4e209606268..366f20f71f9638 100644 --- a/tools/generate_doc_comment.py +++ b/tools/generate_doc_comment.py @@ -11,16 +11,21 @@ # 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. +from __future__ import annotations +import argparse import importlib import inspect import re -import sys +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Callable import paddle # noqa: F401 -def resolve_string_to_obj(path: str): +def load_api_by_name(path: str) -> Callable[..., Any] | None: """ Recursively resolves a string path to a Python object. """ @@ -39,7 +44,7 @@ def resolve_string_to_obj(path: str): # Split the path into its parent and the final object name. # e.g., "paddle.Tensor" -> parent="paddle", child="Tensor" parent_path, child_name = path.rsplit('.', 1) - parent_obj = resolve_string_to_obj(parent_path) + parent_obj = load_api_by_name(parent_path) # If the parent object could not be resolved, we can't find the child. if parent_obj is None: @@ -49,44 +54,58 @@ def resolve_string_to_obj(path: str): return getattr(parent_obj, child_name, None) -def generate_comment_body(doc_diff, pr_id): +def generate_comment_body(doc_diff: str, pr_id: int) -> str: if not doc_diff: return "" - output_lines = [] + output_lines: list[str] = [] base_url = f"http://preview-paddle-pr-{pr_id}.paddle-docs-preview.paddlepaddle.org.cn/documentation/docs/en/api" # Extract API names like 'paddle.autograd.backward' from lines like: # - paddle.autograd.backward (ArgSpec(...), ('document', ...)) # + paddle.autograd.backward (ArgSpec(...), ('document', ...)) - apis = sorted( + apis: list[str] = sorted( set(re.findall(r"^[+]\s*([a-zA-Z0-9_.]+)\s*\(", doc_diff, re.MULTILINE)) ) + # All apis should be loaded, this seems a explicitly check. + unload_apis: list[str] = [] + + if not apis: + return "" for api in apis: - api_obj = resolve_string_to_obj(api) + api_obj = load_api_by_name(api) if api_obj is None: - raise ValueError(f"Could not resolve API path: {api}") + unload_apis.append(api) + continue api_path = api.replace('.', '/') url = f"{base_url}/{api_path}_en.html" if "." in api: parent_path, child_name = api.rsplit('.', 1) - parent_obj = resolve_string_to_obj(parent_path) - if inspect.isclass(parent_obj) and inspect.isfunction(api_obj): + parent_obj = load_api_by_name(parent_path) + if inspect.isclass(parent_obj) and not inspect.isclass(api_obj): parent_api_path = parent_path.replace('.', '/') url = f"{base_url}/{parent_api_path}_en.html#{child_name}" output_lines.append(f"- **{api}**: [Preview]({url})") + unload_error_msg = ( + f"@ooooo-create, following apis cannot be loaded, please check it: {', '.join(unload_apis)}" + if unload_apis + else "" + ) if not output_lines: - return "" + return unload_error_msg - comment_body = """
+ api_links = "\n".join(output_lines) + comment_body = f"""
📚 Preview documentation links for API changes in this PR (Click to expand) +{unload_error_msg} +
@@ -98,25 +117,33 @@ def generate_comment_body(doc_diff, pr_id): The following are preview links for new or modified API documentation in this PR: -{} +{api_links} -""".format("\n".join(output_lines)) +""" return comment_body -if __name__ == "__main__": - if len(sys.argv) < 3: - print( - "Usage: python generate_doc_comment.py " - ) - sys.exit(1) +def cli(): + parser = argparse.ArgumentParser( + description="Generate documentation comment for PR with API changes" + ) + parser.add_argument( + "doc_diff_path", help="Path to the documentation diff file", type=str + ) + parser.add_argument("pr_id", help="Pull request ID", type=int) + return parser.parse_args() + - doc_diff_path = sys.argv[1] - pr_id = sys.argv[2] +def main(): + args = cli() - with open(doc_diff_path, 'r') as f: + with open(args.doc_diff_path, 'r') as f: doc_diff_content = f.read() - comment = generate_comment_body(doc_diff_content, pr_id) + comment = generate_comment_body(doc_diff_content, args.pr_id) print(comment) + + +if __name__ == "__main__": + main()