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

pantsbuild / pants / 26260209689

21 May 2026 11:59PM UTC coverage: 75.453% (-15.7%) from 91.156%
26260209689

Pull #23365

github

web-flow
Merge 5fe873b58 into 7ea655ba0
Pull Request #23365: uv.lock -> pex optimization

5 of 16 new or added lines in 1 file covered. (31.25%)

10118 existing lines in 378 files now uncovered.

54669 of 72454 relevant lines covered (75.45%)

2.31 hits per line

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

84.62
/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
5✔
4

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

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

23
logger = logging.getLogger(__name__)
5✔
24

25

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

35

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

39

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

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

45

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

50

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

55

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

62

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

67

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

72

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

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

81

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

87

88
_scripts_package = "pants.backend.python.dependency_inference.scripts"
5✔
89

90

91
def _is_ignored_string_import(module_path: str, ignored_patterns: tuple[str, ...]) -> bool:
5✔
UNCOV
92
    return any(fnmatchcase(module_path, ignored_pattern) for ignored_pattern in ignored_patterns)
×
93

94

95
async def get_scripts_digest(scripts_package: str, filenames: Iterable[str]) -> Digest:
5✔
96
    scripts = [read_resource(scripts_package, filename) for filename in filenames]
×
97
    assert all(script is not None for script in scripts)
×
98
    path_prefix = scripts_package.replace(".", os.path.sep)
×
99
    contents = [
×
100
        FileContent(os.path.join(path_prefix, relpath), script)
101
        for relpath, script in zip(filenames, scripts)
102
    ]
103

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

115
    digest = await create_digest(CreateDigest(contents))
×
116
    return digest
×
117

118

119
@rule(level=LogLevel.DEBUG)
5✔
120
async def parse_python_dependencies(
5✔
121
    request: ParsePythonDependenciesRequest,
122
    python_infer_subsystem: PythonInferSubsystem,
123
) -> PythonFilesDependencies:
124
    stripped_sources = await strip_source_roots(request.source)
5✔
125
    native_results = await parse_python_deps(
5✔
126
        NativeDependenciesRequest(stripped_sources.snapshot.digest)
127
    )
128

129
    path_to_deps = {}
5✔
130
    for path, native_result in native_results.path_to_deps.items():
5✔
131
        imports = dict(native_result.imports)
5✔
132
        assets = set()
5✔
133

134
        if python_infer_subsystem.string_imports or python_infer_subsystem.assets:
5✔
135
            for string, line in native_result.string_candidates.items():
1✔
136
                if (
1✔
137
                    python_infer_subsystem.string_imports
138
                    and not _is_ignored_string_import(
139
                        string, python_infer_subsystem.string_import_ignore
140
                    )
141
                    and string.count(".") >= python_infer_subsystem.string_imports_min_dots
142
                    and all(part.isidentifier() for part in string.split("."))
143
                ):
UNCOV
144
                    imports.setdefault(string, (line, True))
×
145
                if (
1✔
146
                    python_infer_subsystem.assets
147
                    and string.count("/") >= python_infer_subsystem.assets_min_slashes
148
                ):
149
                    assets.add(string)
1✔
150

151
        explicit_deps = dict(native_result.explicit_dependencies)
5✔
152

153
        path_to_deps[path] = PythonFileDependencies(
5✔
154
            ParsedPythonImports(
155
                (key, ParsedPythonImportInfo(*value)) for key, value in imports.items()
156
            ),
157
            ParsedPythonAssetPaths(sorted(assets)),
158
            ExplicitPythonDependencies(FrozenDict(explicit_deps)),
159
        )
160
    return PythonFilesDependencies(FrozenDict(path_to_deps))
5✔
161

162

163
def rules():
5✔
164
    return [
5✔
165
        *collect_rules(),
166
    ]
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