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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

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

85.33
/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).
3
from __future__ import annotations
4✔
4

5
import logging
4✔
6
import os
4✔
7
from collections.abc import Iterable
4✔
8
from dataclasses import dataclass
4✔
9

10
from pants.backend.python.dependency_inference.subsystem import PythonInferSubsystem
4✔
11
from pants.core.util_rules.source_files import SourceFiles
4✔
12
from pants.core.util_rules.stripped_source_files import strip_source_roots
4✔
13
from pants.engine.collection import DeduplicatedCollection
4✔
14
from pants.engine.fs import CreateDigest, Digest, FileContent
4✔
15
from pants.engine.internals.native_engine import NativeDependenciesRequest
4✔
16
from pants.engine.intrinsics import create_digest, parse_python_deps
4✔
17
from pants.engine.rules import collect_rules, rule
4✔
18
from pants.util.frozendict import FrozenDict
4✔
19
from pants.util.logging import LogLevel
4✔
20
from pants.util.resources import read_resource
4✔
21

22
logger = logging.getLogger(__name__)
4✔
23

24

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

34

35
class ParsedPythonImports(FrozenDict[str, ParsedPythonImportInfo]):
4✔
36
    """All the discovered imports from a Python source file mapped to the relevant info."""
37

38

39
class ParsedPythonAssetPaths(DeduplicatedCollection[str]):
4✔
40
    """All the discovered possible assets from a Python source file."""
41

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

44

45
# Map from argument of `pants: infer-dep(...)` to line number at which it appears.
46
class ExplicitPythonDependencies(FrozenDict[str, int]):
4✔
47
    """Dependencies provided via the # pants: infer-dep() pragma."""
48

49

50
# TODO: Use the Native* eqivalents of these classes directly? Would require
51
#  conversion to the component classes in Rust code. Might require passing
52
#  the PythonInferSubsystem settings through to Rust and acting on them there.
53

54

55
@dataclass(frozen=True)
4✔
56
class PythonFileDependencies:
4✔
57
    imports: ParsedPythonImports
4✔
58
    assets: ParsedPythonAssetPaths
4✔
59
    explicit_dependencies: ExplicitPythonDependencies
4✔
60

61

62
@dataclass(frozen=True)
4✔
63
class PythonFilesDependencies:
4✔
64
    path_to_deps: FrozenDict[str, PythonFileDependencies]
4✔
65

66

67
@dataclass(frozen=True)
4✔
68
class ParsePythonDependenciesRequest:
4✔
69
    source: SourceFiles
4✔
70

71

72
@dataclass(frozen=True)
4✔
73
class PythonDependencyVisitor:
4✔
74
    """Wraps a subclass of DependencyVisitorBase."""
75

76
    digest: Digest  # The file contents for the visitor
4✔
77
    classname: str  # The full classname, e.g., _my_custom_dep_parser.MyCustomVisitor
4✔
78
    env: FrozenDict[str, str]  # Set these env vars when invoking the visitor
4✔
79

80

81
@dataclass(frozen=True)
4✔
82
class ParserScript:
4✔
83
    digest: Digest
4✔
84
    env: FrozenDict[str, str]
4✔
85

86

87
_scripts_package = "pants.backend.python.dependency_inference.scripts"
4✔
88

89

90
async def get_scripts_digest(scripts_package: str, filenames: Iterable[str]) -> Digest:
4✔
91
    scripts = [read_resource(scripts_package, filename) for filename in filenames]
×
92
    assert all(script is not None for script in scripts)
×
93
    path_prefix = scripts_package.replace(".", os.path.sep)
×
94
    contents = [
×
95
        FileContent(os.path.join(path_prefix, relpath), script)
96
        for relpath, script in zip(filenames, scripts)
97
    ]
98

99
    # Python 2 requires all the intermediate __init__.py to exist in the sandbox.
100
    package = scripts_package
×
101
    while package:
×
102
        contents.append(
×
103
            FileContent(
104
                os.path.join(package.replace(".", os.path.sep), "__init__.py"),
105
                read_resource(package, "__init__.py"),
106
            )
107
        )
108
        package = package.rpartition(".")[0]
×
109

110
    digest = await create_digest(CreateDigest(contents))
×
111
    return digest
×
112

113

114
@rule(level=LogLevel.DEBUG)
4✔
115
async def parse_python_dependencies(
4✔
116
    request: ParsePythonDependenciesRequest,
117
    python_infer_subsystem: PythonInferSubsystem,
118
) -> PythonFilesDependencies:
119
    stripped_sources = await strip_source_roots(request.source)
4✔
120
    native_results = await parse_python_deps(
4✔
121
        NativeDependenciesRequest(stripped_sources.snapshot.digest)
122
    )
123

124
    path_to_deps = {}
4✔
125
    for path, native_result in native_results.path_to_deps.items():
4✔
126
        imports = dict(native_result.imports)
4✔
127
        assets = set()
4✔
128

129
        if python_infer_subsystem.string_imports or python_infer_subsystem.assets:
4✔
130
            for string, line in native_result.string_candidates.items():
1✔
131
                if (
1✔
132
                    python_infer_subsystem.string_imports
133
                    and string.count(".") >= python_infer_subsystem.string_imports_min_dots
134
                    and all(part.isidentifier() for part in string.split("."))
135
                ):
UNCOV
136
                    imports.setdefault(string, (line, True))
×
137
                if (
1✔
138
                    python_infer_subsystem.assets
139
                    and string.count("/") >= python_infer_subsystem.assets_min_slashes
140
                ):
141
                    assets.add(string)
1✔
142

143
        explicit_deps = dict(native_result.explicit_dependencies)
4✔
144

145
        path_to_deps[path] = PythonFileDependencies(
4✔
146
            ParsedPythonImports(
147
                (key, ParsedPythonImportInfo(*value)) for key, value in imports.items()
148
            ),
149
            ParsedPythonAssetPaths(sorted(assets)),
150
            ExplicitPythonDependencies(FrozenDict(explicit_deps)),
151
        )
152
    return PythonFilesDependencies(FrozenDict(path_to_deps))
4✔
153

154

155
def rules():
4✔
156
    return [
4✔
157
        *collect_rules(),
158
    ]
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