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..642bb1f87da80c 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,43 @@ jobs: 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<> $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: | 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 new file mode 100644 index 00000000000000..366f20f71f9638 --- /dev/null +++ b/tools/generate_doc_comment.py @@ -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"""
+📚 Preview documentation links for API changes in this PR (Click to expand) + +{unload_error_msg} + + + + + +
+â„šī¸ 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: + +{api_links} + +
""" + + 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) + + +if __name__ == "__main__": + main()