• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 22206482340

20 Feb 2026 12:44AM UTC coverage: 80.376% (+0.05%) from 80.324%
22206482340

Pull #23066

github

web-flow
Merge ddfa8ddd7 into 56952304a
Pull Request #23066: Extract JavaScript backend test resources to files

73 of 73 new or added lines in 2 files covered. (100.0%)

1093 existing lines in 48 files now uncovered.

78880 of 98139 relevant lines covered (80.38%)

3.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.69
/build-support/bin/external_tool/python.py
1
# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
import ast
1✔
4
import json
1✔
5
import logging
1✔
6
import os
1✔
7
import textwrap
1✔
8
from collections.abc import Generator
1✔
9
from pathlib import Path
1✔
10
from typing import Any, TypeVar
1✔
11

12
logger = logging.getLogger(__name__)
1✔
13
T = TypeVar("T")
1✔
14

15

16
def _format_value(value: Any, indent: int = 4) -> str:
1✔
17
    # Do some extra work to format lists with a trailing comma to minimize
18
    # conflict with ruff/black, otherwise just json.dumps
19
    if isinstance(value, list) and all(isinstance(item, str) for item in value):
1✔
20
        if not value:
1✔
21
            return "[]"
×
22
        spaces = " " * indent
1✔
23
        formatted_items = [f'{spaces}"{item}",' for item in value]
1✔
24
        return "[\n" + "\n".join(formatted_items) + "\n]"
1✔
25
    return json.dumps(value, indent=indent)
1✔
26

27

28
def get_class_variables(file_path: Path, class_name: str, *, variables: type[T]) -> T:
1✔
29
    """Reads a Python file and retrieves the values of specified class variables."""
30

31
    logger.info("parsing %s variables in %s", class_name, file_path)
×
32
    with open(file_path, encoding="utf-8") as file:
×
UNCOV
33
        source_code = file.read()
×
34

UNCOV
35
    tree = ast.parse(source_code)
×
UNCOV
36
    values = {}
×
37

UNCOV
38
    for node in tree.body:
×
UNCOV
39
        if isinstance(node, ast.ClassDef) and node.name == class_name:
×
UNCOV
40
            for stmt in node.body:
×
UNCOV
41
                if isinstance(stmt, ast.Assign):
×
UNCOV
42
                    for target in stmt.targets:
×
UNCOV
43
                        if isinstance(target, ast.Name) and target.id in variables.__annotations__:
×
UNCOV
44
                            values[target.id] = ast.literal_eval(stmt.value)
×
45

UNCOV
46
    return variables(**values)
×
47

48

49
def replace_class_variables(file_path: Path, class_name: str, replacements: dict[str, Any]) -> None:
1✔
50
    """Reads a Python file, searches for a class by name, and replaces specified class variables
51
    with new values."""
52
    with open(file_path, encoding="utf-8") as file:
1✔
53
        lines = file.readlines()
1✔
54

55
    tree = ast.parse("".join(lines))
1✔
56

57
    class_var_ranges = {}
1✔
58
    for node in tree.body:
1✔
59
        if isinstance(node, ast.ClassDef) and node.name == class_name:
1✔
60
            for stmt in node.body:
1✔
61
                if isinstance(stmt, ast.Assign):
1✔
62
                    for target in stmt.targets:
1✔
63
                        if isinstance(target, ast.Name) and target.id in replacements:
1✔
64
                            start_line = stmt.lineno - 1
1✔
65
                            end_line = (
1✔
66
                                stmt.end_lineno if stmt.end_lineno is not None else start_line
67
                            )
68
                            class_var_ranges[target.id] = (start_line, end_line)
1✔
69

70
    logger.debug("class_var_ranges: %s", class_var_ranges)
1✔
71

72
    prev_end = 0
1✔
73
    with open(file_path, "w", encoding="utf-8") as file:
1✔
74
        for var, (start, end) in class_var_ranges.items():
1✔
75
            file.writelines(lines[prev_end:start])
1✔
76
            line = textwrap.indent(
1✔
77
                f"{var} = {_format_value(replacements[var])}\n",
78
                "    ",
79
            )
80
            file.writelines([line])
1✔
81
            prev_end = end
1✔
82
        file.writelines(lines[prev_end:])
1✔
83

84

85
def find_modules_with_subclasses(
1✔
86
    directory: Path,
87
    *,
88
    base_classes: set[str],
89
    exclude: set[str],
90
) -> Generator[tuple[Path, str], None, None]:
91
    """Recursively finds Python modules that contain classes subclassing a given base class."""
92

UNCOV
93
    for root, _, files in os.walk(directory):
×
UNCOV
94
        for file in files:
×
UNCOV
95
            if file.endswith(".py"):
×
UNCOV
96
                file_path = Path(root) / file
×
UNCOV
97
                source_code = file_path.read_text()
×
98

UNCOV
99
                tree = ast.parse(source_code)
×
UNCOV
100
                for node in tree.body:
×
UNCOV
101
                    if isinstance(node, ast.ClassDef) and node.name not in exclude:
×
UNCOV
102
                        for base in node.bases:
×
UNCOV
103
                            if isinstance(base, ast.Name) and base.id in base_classes:
×
UNCOV
104
                                yield file_path, node.name
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc