mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 03:44:55 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			149 lines
		
	
	
	
		
			4.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			149 lines
		
	
	
	
		
			4.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python3
 | 
						|
"""
 | 
						|
Check the output of running Sphinx in nit-picky mode (missing references).
 | 
						|
"""
 | 
						|
import argparse
 | 
						|
import csv
 | 
						|
import os
 | 
						|
import re
 | 
						|
import sys
 | 
						|
from pathlib import Path
 | 
						|
 | 
						|
# Exclude these whether they're dirty or clean,
 | 
						|
# because they trigger a rebuild of dirty files.
 | 
						|
EXCLUDE_FILES = {
 | 
						|
    "Doc/whatsnew/changelog.rst",
 | 
						|
}
 | 
						|
 | 
						|
# Subdirectories of Doc/ to exclude.
 | 
						|
EXCLUDE_SUBDIRS = {
 | 
						|
    ".env",
 | 
						|
    ".venv",
 | 
						|
    "env",
 | 
						|
    "includes",
 | 
						|
    "venv",
 | 
						|
}
 | 
						|
 | 
						|
PATTERN = re.compile(r"(?P<file>[^:]+):(?P<line>\d+): WARNING: (?P<msg>.+)")
 | 
						|
 | 
						|
 | 
						|
def check_and_annotate(warnings: list[str], files_to_check: str) -> None:
 | 
						|
    """
 | 
						|
    Convert Sphinx warning messages to GitHub Actions.
 | 
						|
 | 
						|
    Converts lines like:
 | 
						|
        .../Doc/library/cgi.rst:98: WARNING: reference target not found
 | 
						|
    to:
 | 
						|
        ::warning file=.../Doc/library/cgi.rst,line=98::reference target not found
 | 
						|
 | 
						|
    Non-matching lines are echoed unchanged.
 | 
						|
 | 
						|
    see:
 | 
						|
    https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message
 | 
						|
    """
 | 
						|
    files_to_check = next(csv.reader([files_to_check]))
 | 
						|
    for warning in warnings:
 | 
						|
        if any(filename in warning for filename in files_to_check):
 | 
						|
            if match := PATTERN.fullmatch(warning):
 | 
						|
                print("::warning file={file},line={line}::{msg}".format_map(match))
 | 
						|
 | 
						|
 | 
						|
def fail_if_regression(
 | 
						|
    warnings: list[str], files_with_expected_nits: set[str], files_with_nits: set[str]
 | 
						|
) -> int:
 | 
						|
    """
 | 
						|
    Ensure some files always pass Sphinx nit-picky mode (no missing references).
 | 
						|
    These are files which are *not* in .nitignore.
 | 
						|
    """
 | 
						|
    all_rst = {
 | 
						|
        str(rst)
 | 
						|
        for rst in Path("Doc/").rglob("*.rst")
 | 
						|
        if rst.parts[1] not in EXCLUDE_SUBDIRS
 | 
						|
    }
 | 
						|
    should_be_clean = all_rst - files_with_expected_nits - EXCLUDE_FILES
 | 
						|
    problem_files = sorted(should_be_clean & files_with_nits)
 | 
						|
    if problem_files:
 | 
						|
        print("\nError: must not contain warnings:\n")
 | 
						|
        for filename in problem_files:
 | 
						|
            print(filename)
 | 
						|
            for warning in warnings:
 | 
						|
                if filename in warning:
 | 
						|
                    if match := PATTERN.fullmatch(warning):
 | 
						|
                        print("  {line}: {msg}".format_map(match))
 | 
						|
        return -1
 | 
						|
    return 0
 | 
						|
 | 
						|
 | 
						|
def fail_if_improved(
 | 
						|
    files_with_expected_nits: set[str], files_with_nits: set[str]
 | 
						|
) -> int:
 | 
						|
    """
 | 
						|
    We may have fixed warnings in some files so that the files are now completely clean.
 | 
						|
    Good news! Let's add them to .nitignore to prevent regression.
 | 
						|
    """
 | 
						|
    files_with_no_nits = files_with_expected_nits - files_with_nits
 | 
						|
    if files_with_no_nits:
 | 
						|
        print("\nCongratulations! You improved:\n")
 | 
						|
        for filename in sorted(files_with_no_nits):
 | 
						|
            print(filename)
 | 
						|
        print("\nPlease remove from Doc/tools/.nitignore\n")
 | 
						|
        return -1
 | 
						|
    return 0
 | 
						|
 | 
						|
 | 
						|
def main() -> int:
 | 
						|
    parser = argparse.ArgumentParser()
 | 
						|
    parser.add_argument(
 | 
						|
        "--check-and-annotate",
 | 
						|
        help="Comma-separated list of files to check, "
 | 
						|
        "and annotate those with warnings on GitHub Actions",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--fail-if-regression",
 | 
						|
        action="store_true",
 | 
						|
        help="Fail if known-good files have warnings",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--fail-if-improved",
 | 
						|
        action="store_true",
 | 
						|
        help="Fail if new files with no nits are found",
 | 
						|
    )
 | 
						|
    args = parser.parse_args()
 | 
						|
    exit_code = 0
 | 
						|
 | 
						|
    wrong_directory_msg = "Must run this script from the repo root"
 | 
						|
    assert Path("Doc").exists() and Path("Doc").is_dir(), wrong_directory_msg
 | 
						|
 | 
						|
    with Path("Doc/sphinx-warnings.txt").open() as f:
 | 
						|
        warnings = f.read().splitlines()
 | 
						|
 | 
						|
    cwd = str(Path.cwd()) + os.path.sep
 | 
						|
    files_with_nits = {
 | 
						|
        warning.removeprefix(cwd).split(":")[0]
 | 
						|
        for warning in warnings
 | 
						|
        if "Doc/" in warning
 | 
						|
    }
 | 
						|
 | 
						|
    with Path("Doc/tools/.nitignore").open() as clean_files:
 | 
						|
        files_with_expected_nits = {
 | 
						|
            filename.strip()
 | 
						|
            for filename in clean_files
 | 
						|
            if filename.strip() and not filename.startswith("#")
 | 
						|
        }
 | 
						|
 | 
						|
    if args.check_and_annotate:
 | 
						|
        check_and_annotate(warnings, args.check_and_annotate)
 | 
						|
 | 
						|
    if args.fail_if_regression:
 | 
						|
        exit_code += fail_if_regression(
 | 
						|
            warnings, files_with_expected_nits, files_with_nits
 | 
						|
        )
 | 
						|
 | 
						|
    if args.fail_if_improved:
 | 
						|
        exit_code += fail_if_improved(files_with_expected_nits, files_with_nits)
 | 
						|
 | 
						|
    return exit_code
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    sys.exit(main())
 |