Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/Preview-Url-Comment.yml
Original file line number Diff line number Diff line change
@@ -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<<EOF'
echo "$COMMENT_BODY"
echo EOF
} >> $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
43 changes: 41 additions & 2 deletions .github/workflows/_Doc-Preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,57 @@ 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 /
source ${{ github.workspace }}/../../../proxy
bash "/tmp/entrypoint.sh"
'

- name: Generate Comment Body
id: generate_comment
run: |
comment_body=$(docker exec ${{ 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<<EOF" >> $GITHUB_OUTPUT
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

- 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: |
Expand Down
5 changes: 4 additions & 1 deletion python/paddle/nn/functional/conv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
149 changes: 149 additions & 0 deletions tools/generate_doc_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# 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.
from __future__ import annotations

import argparse
import importlib
import inspect
import re
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from collections.abc import Callable

import paddle # noqa: F401


def load_api_by_name(path: str) -> Callable[..., Any] | None:
"""
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 = 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:
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: str, pr_id: int) -> str:
if not doc_diff:
return ""

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: 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 = load_api_by_name(api)

if api_obj is None:
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 = 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 unload_error_msg

api_links = "\n".join(output_lines)
comment_body = f"""<details>
<summary>📚 Preview documentation links for API changes in this PR (Click to expand)</summary>

{unload_error_msg}

<table>
<tr>
<td>
ℹ️ <b>Preview Notice</b><br>
Please wait for the <code>Doc-Preview</code> workflow to complete before clicking the preview links below, otherwise you may see outdated content.
</td>
</tr>
</table>

The following are preview links for new or modified API documentation in this PR:

{api_links}

</details>"""

return comment_body


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()


def main():
args = cli()

with open(args.doc_diff_path, 'r') as f:
doc_diff_content = f.read()

comment = generate_comment_body(doc_diff_content, args.pr_id)
print(comment)
Comment thread
ooooo-create marked this conversation as resolved.


if __name__ == "__main__":
main()
Loading