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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

0.0
/src/python/pants/backend/python/dependency_inference/parse_python_dependencies.py
1
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
UNCOV
3
from __future__ import annotations
×
4

UNCOV
5
import logging
×
UNCOV
6
import os
×
UNCOV
7
from collections.abc import Iterable
×
UNCOV
8
from dataclasses import dataclass
×
9

UNCOV
10
from pants.backend.python.dependency_inference.subsystem import PythonInferSubsystem
×
UNCOV
11
from pants.backend.python.target_types import PythonSourceField
×
UNCOV
12
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
UNCOV
13
from pants.core.util_rules.source_files import SourceFilesRequest
×
UNCOV
14
from pants.core.util_rules.stripped_source_files import strip_source_roots
×
UNCOV
15
from pants.engine.collection import DeduplicatedCollection
×
UNCOV
16
from pants.engine.fs import CreateDigest, Digest, FileContent
×
UNCOV
17
from pants.engine.internals.native_engine import NativeDependenciesRequest
×
UNCOV
18
from pants.engine.intrinsics import create_digest, parse_python_deps
×
UNCOV
19
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
20
from pants.util.frozendict import FrozenDict
×
UNCOV
21
from pants.util.logging import LogLevel
×
UNCOV
22
from pants.util.resources import read_resource
×
23

UNCOV
24
logger = logging.getLogger(__name__)
×
25

26

UNCOV
27
@dataclass(frozen=True, order=True)
×
UNCOV
28
class ParsedPythonImportInfo:
×
UNCOV
29
    lineno: int
×
30
    # An import is considered "weak" if we're unsure if a dependency will exist between the parsed
31
    # file and the parsed import.
32
    # Examples of "weak" imports include string imports (if enabled) or those inside a try block
33
    # which has a handler catching ImportError.
UNCOV
34
    weak: bool
×
35

36

UNCOV
37
class ParsedPythonImports(FrozenDict[str, ParsedPythonImportInfo]):
×
38
    """All the discovered imports from a Python source file mapped to the relevant info."""
39

40

UNCOV
41
class ParsedPythonAssetPaths(DeduplicatedCollection[str]):
×
42
    """All the discovered possible assets from a Python source file."""
43

44
    # N.B. Don't set `sort_input`, as the input is already sorted
45

46

UNCOV
47
@dataclass(frozen=True)
×
UNCOV
48
class ParsedPythonDependencies:
×
UNCOV
49
    imports: ParsedPythonImports
×
UNCOV
50
    assets: ParsedPythonAssetPaths
×
51

52

UNCOV
53
@dataclass(frozen=True)
×
UNCOV
54
class ParsePythonDependenciesRequest:
×
UNCOV
55
    source: PythonSourceField
×
UNCOV
56
    interpreter_constraints: InterpreterConstraints
×
57

58

UNCOV
59
@dataclass(frozen=True)
×
UNCOV
60
class PythonDependencyVisitor:
×
61
    """Wraps a subclass of DependencyVisitorBase."""
62

UNCOV
63
    digest: Digest  # The file contents for the visitor
×
UNCOV
64
    classname: str  # The full classname, e.g., _my_custom_dep_parser.MyCustomVisitor
×
UNCOV
65
    env: FrozenDict[str, str]  # Set these env vars when invoking the visitor
×
66

67

UNCOV
68
@dataclass(frozen=True)
×
UNCOV
69
class ParserScript:
×
UNCOV
70
    digest: Digest
×
UNCOV
71
    env: FrozenDict[str, str]
×
72

73

UNCOV
74
_scripts_package = "pants.backend.python.dependency_inference.scripts"
×
75

76

UNCOV
77
async def get_scripts_digest(scripts_package: str, filenames: Iterable[str]) -> Digest:
×
78
    scripts = [read_resource(scripts_package, filename) for filename in filenames]
×
79
    assert all(script is not None for script in scripts)
×
80
    path_prefix = scripts_package.replace(".", os.path.sep)
×
81
    contents = [
×
82
        FileContent(os.path.join(path_prefix, relpath), script)
83
        for relpath, script in zip(filenames, scripts)
84
    ]
85

86
    # Python 2 requires all the intermediate __init__.py to exist in the sandbox.
87
    package = scripts_package
×
88
    while package:
×
89
        contents.append(
×
90
            FileContent(
91
                os.path.join(package.replace(".", os.path.sep), "__init__.py"),
92
                read_resource(package, "__init__.py"),
93
            )
94
        )
95
        package = package.rpartition(".")[0]
×
96

97
    digest = await create_digest(CreateDigest(contents))
×
98
    return digest
×
99

100

UNCOV
101
@rule(level=LogLevel.DEBUG)
×
UNCOV
102
async def parse_python_dependencies(
×
103
    request: ParsePythonDependenciesRequest,
104
    python_infer_subsystem: PythonInferSubsystem,
105
) -> ParsedPythonDependencies:
106
    stripped_sources = await strip_source_roots(**implicitly(SourceFilesRequest([request.source])))
×
107
    # We operate on PythonSourceField, which should be one file.
108
    assert len(stripped_sources.snapshot.files) == 1
×
109

110
    native_result = await parse_python_deps(
×
111
        NativeDependenciesRequest(stripped_sources.snapshot.digest)
112
    )
113
    imports = dict(native_result.imports)
×
114
    assets = set()
×
115

116
    if python_infer_subsystem.string_imports or python_infer_subsystem.assets:
×
117
        for string, line in native_result.string_candidates.items():
×
118
            if (
×
119
                python_infer_subsystem.string_imports
120
                and string.count(".") >= python_infer_subsystem.string_imports_min_dots
121
                and all(part.isidentifier() for part in string.split("."))
122
            ):
123
                imports.setdefault(string, (line, True))
×
124
            if (
×
125
                python_infer_subsystem.assets
126
                and string.count("/") >= python_infer_subsystem.assets_min_slashes
127
            ):
128
                assets.add(string)
×
129

130
    return ParsedPythonDependencies(
×
131
        ParsedPythonImports(
132
            (key, ParsedPythonImportInfo(*value)) for key, value in imports.items()
133
        ),
134
        ParsedPythonAssetPaths(sorted(assets)),
135
    )
136

137

UNCOV
138
def rules():
×
UNCOV
139
    return [
×
140
        *collect_rules(),
141
    ]
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

© 2025 Coveralls, Inc