Files
paddlepaddle--paddle/tools/check_code_block_format.py
2026-07-13 12:40:42 +08:00

267 lines
7.8 KiB
Python

# 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.
"""
Tool to check if the code-block directives in the given files follow proper format.
The following format issues are checked:
- The code under code-block directive should be indented properly.
- The code-block directive should be followed by a blank line.
You can run this script as follows:
python tools/check_code_block_format.py <file1> <file2> ...
If you want to check all Python and C++ files under a directory, you can provide the directory path as an argument:
python tools/check_code_block_format.py <directory_path>
For example:
python tools/check_code_block_format.py python paddle
"""
import argparse
import io
import re
import sys
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
REGEX_CODE_BLOCK = re.compile(r"(?P<indent>\s*)..\s+code-block::\s*(\w+)?")
REGEX_CODE_BLOCK_ATTRS = re.compile(r"(?P<indent>\s*):(\w+):\s*(.*)?")
def with_color(text: str, color_code: str) -> str:
return f"\033[{color_code}m{text}\033[0m"
def with_red(text: str) -> str:
return with_color(text, "31")
def with_cyan(text: str) -> str:
return with_color(text, "36")
def with_blue(text: str) -> str:
return with_color(text, "34")
@dataclass
class CodeBlock:
lines: list[str]
start_lineno: int
@dataclass
class CodeBlockModifySuggestion:
lineno: int
message: str
class Diagnostic:
def __init__(self, start: int, message: str):
self.start = start
self.message = message
def is_blank_line(line: str) -> bool:
return line.strip() == ""
class CodeBlockIndentationDiagnostic(Diagnostic):
def __init__(
self,
start: int,
code_block: CodeBlock,
suggestions: list[CodeBlockModifySuggestion] = [],
):
super().__init__(
start,
"The code under code-block directive should be indented properly. The issue found in:\n"
+ format_code_block_with_suggestions(code_block, suggestions),
)
class CodeBlockFollowsBlankLineDiagnostic(Diagnostic):
def __init__(
self,
start: int,
code_block: CodeBlock,
suggestions: list[CodeBlockModifySuggestion] = [],
):
super().__init__(
start,
"The code-block directive should be followed by a blank line. The issue found in:\n"
+ format_code_block_with_suggestions(code_block, suggestions),
)
def cli() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description='Check if the code-block follows a blank line format.'
)
parser.add_argument(
'files', type=str, nargs='+', help='files to be checked'
)
args = parser.parse_args()
return args
def expand_glob(files) -> list[Path]:
expanded = []
for file in files:
path = Path(file)
if path.is_dir():
expanded.extend(path.glob('**/*.py'))
expanded.extend(path.glob('**/*.cc'))
expanded.extend(path.glob('**/*.h'))
else:
expanded.append(path)
return expanded
def format_code_block_with_suggestions(
code_block: CodeBlock, suggestions: list[CodeBlockModifySuggestion]
) -> str:
formatted_lines = []
for line_offset, line in enumerate(code_block.lines):
lineno_str = with_cyan(f"{code_block.start_lineno + line_offset:>5}|")
formatted_lines.append(f"{lineno_str} {line}")
for suggestion in suggestions:
if suggestion.lineno == line_offset:
formatted_lines.append(
f" | {with_red('^')} {with_blue(suggestion.message)}\n"
)
return ''.join(formatted_lines)
def extract_code_blocks(
file: io.TextIOWrapper,
) -> Iterator[CodeBlock]:
lines_iter = iter(enumerate(file))
while True:
next_item = next(lines_iter, None)
if next_item is None:
break
lineno, line = next_item
match = REGEX_CODE_BLOCK.match(line)
if match:
indent = match.group('indent')
indent_size = len(indent)
code_block_start_lineno = lineno + 1
code_block_lines = [line]
indent_regex = re.compile(rf"\s{{{indent_size + 1}}}")
while True:
next_item = next(lines_iter, None)
if next_item is None:
break
lineno, line = next_item
if not indent_regex.match(line) and not is_blank_line(line):
break
code_block_lines.append(line)
yield CodeBlock(code_block_lines, code_block_start_lineno)
def check_code_block_indentation(
code_block: CodeBlock,
) -> list[Diagnostic]:
suggestion = CodeBlockModifySuggestion(
lineno=0, message="Next line should be indented."
)
diagnostic = CodeBlockIndentationDiagnostic(
code_block.start_lineno,
code_block,
[suggestion],
)
if len(code_block.lines) == 1:
suggestion.lineno = 1
return [diagnostic]
for line_offset, line in enumerate(code_block.lines[1:], 1):
if not is_blank_line(line) and not REGEX_CODE_BLOCK_ATTRS.match(line):
return []
suggestion.lineno = line_offset
return [diagnostic]
def check_code_block_follows_blank_line(
code_block: CodeBlock,
) -> list[Diagnostic]:
suggestion = CodeBlockModifySuggestion(
lineno=0, message="Add a blank line here."
)
diagnostic = CodeBlockFollowsBlankLineDiagnostic(
code_block.start_lineno,
code_block,
[suggestion],
)
if len(code_block.lines) < 2:
return []
def check_lines_starts_with_blank_line(
lines: list[str], start_lineno: int
) -> bool:
if not lines:
return True
first_line = lines[0]
if REGEX_CODE_BLOCK_ATTRS.match(first_line):
return check_lines_starts_with_blank_line(
lines[1:], start_lineno + 1
)
suggestion.lineno = start_lineno
return is_blank_line(first_line)
if not check_lines_starts_with_blank_line(code_block.lines[1:], 1):
return [diagnostic]
return []
def check_code_block_format(file: io.TextIOWrapper) -> list[Diagnostic]:
diagnostics: list[Diagnostic] = []
for code_block in extract_code_blocks(file):
diagnostics.extend(check_code_block_indentation(code_block))
diagnostics.extend(check_code_block_follows_blank_line(code_block))
return diagnostics
def show_diagnostic(file: Path, diagnostic: Diagnostic) -> None:
print(
f"{with_blue(str(file))}:{with_cyan(str(diagnostic.start))}: "
f"{with_red('error:')} {diagnostic.message}"
)
def main():
args = cli()
files = args.files
diagnostics: list[tuple[Path, list[Diagnostic]]] = []
for file in expand_glob(files):
with open(file, 'r') as f:
file_diagnostics = check_code_block_format(f)
for diagnostic in file_diagnostics:
diagnostics.append((file, file_diagnostics))
if diagnostics:
for file, file_diagnostics in diagnostics:
for diagnostic in file_diagnostics:
show_diagnostic(file, diagnostic)
sys.exit(1)
if __name__ == "__main__":
main()