• 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/openapi/dependency_inference.py
1
# Copyright 2022 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 contextlib
×
UNCOV
6
import json
×
UNCOV
7
import os.path
×
UNCOV
8
from collections.abc import Mapping
×
UNCOV
9
from dataclasses import dataclass
×
UNCOV
10
from typing import Any
×
11

UNCOV
12
import yaml
×
13

UNCOV
14
from pants.backend.openapi.target_types import (
×
15
    OPENAPI_FILE_EXTENSIONS,
16
    OpenApiDocumentDependenciesField,
17
    OpenApiDocumentField,
18
    OpenApiSourceDependenciesField,
19
    OpenApiSourceField,
20
)
UNCOV
21
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
×
UNCOV
22
from pants.base.specs import FileLiteralSpec, RawSpecs
×
UNCOV
23
from pants.engine.fs import Digest
×
UNCOV
24
from pants.engine.internals.graph import (
×
25
    determine_explicitly_provided_dependencies,
26
    hydrate_sources,
27
    resolve_targets,
28
)
UNCOV
29
from pants.engine.internals.selectors import concurrently
×
UNCOV
30
from pants.engine.intrinsics import get_digest_contents
×
UNCOV
31
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
32
from pants.engine.target import (
×
33
    DependenciesRequest,
34
    FieldSet,
35
    HydrateSourcesRequest,
36
    InferDependenciesRequest,
37
    InferredDependencies,
38
)
UNCOV
39
from pants.engine.unions import UnionRule
×
UNCOV
40
from pants.util.frozendict import FrozenDict
×
41

42

UNCOV
43
@dataclass(frozen=True)
×
UNCOV
44
class ParseOpenApiSources:
×
UNCOV
45
    sources_digest: Digest
×
UNCOV
46
    paths: tuple[str, ...]
×
47

48

UNCOV
49
@dataclass(frozen=True)
×
UNCOV
50
class OpenApiDependencies:
×
UNCOV
51
    dependencies: FrozenDict[str, frozenset[str]]
×
52

53

UNCOV
54
@rule
×
UNCOV
55
async def parse_openapi_sources(request: ParseOpenApiSources) -> OpenApiDependencies:
×
56
    digest_contents = await get_digest_contents(request.sources_digest)
×
57
    dependencies: dict[str, frozenset[str]] = {}
×
58

59
    for digest_content in digest_contents:
×
60
        spec = None
×
61

62
        if digest_content.path.endswith(".json"):
×
63
            with contextlib.suppress(json.JSONDecodeError):
×
64
                spec = json.loads(digest_content.content)
×
65
        elif digest_content.path.endswith(".yaml") or digest_content.path.endswith(".yml"):
×
66
            with contextlib.suppress(yaml.YAMLError):
×
67
                spec = yaml.safe_load(digest_content.content)
×
68

69
        if not spec or not isinstance(spec, dict):
×
70
            dependencies[digest_content.path] = frozenset()
×
71
            continue
×
72

73
        dependencies[digest_content.path] = _find_local_refs(digest_content.path, spec)
×
74

75
    return OpenApiDependencies(dependencies=FrozenDict(dependencies))
×
76

77

UNCOV
78
def _find_local_refs(path: str, d: Mapping[str, Any]) -> frozenset[str]:
×
79
    local_refs: set[str] = set()
×
80

81
    for k, v in d.items():
×
82
        if isinstance(v, dict):
×
83
            local_refs.update(_find_local_refs(path, v))
×
84
        elif k == "$ref" and isinstance(v, str):
×
85
            # https://swagger.io/specification/#reference-object
86
            # https://datatracker.ietf.org/doc/html/draft-pbryan-zyp-json-ref-03
87
            v = v.split("#", 1)[0]
×
88

89
            if any(v.endswith(ext) for ext in OPENAPI_FILE_EXTENSIONS) and "://" not in v:
×
90
                # Resolution is performed relative to the referring document.
91
                normalized = os.path.normpath(os.path.join(os.path.dirname(path), v))
×
92

93
                if not normalized.startswith("../"):
×
94
                    local_refs.add(normalized)
×
95

96
    return frozenset(local_refs)
×
97

98

99
# -----------------------------------------------------------------------------------------------
100
# `openapi_document` dependency inference
101
# -----------------------------------------------------------------------------------------------
102

103

UNCOV
104
@dataclass(frozen=True)
×
UNCOV
105
class OpenApiDocumentDependenciesInferenceFieldSet(FieldSet):
×
UNCOV
106
    required_fields = (OpenApiDocumentField, OpenApiDocumentDependenciesField)
×
107

UNCOV
108
    sources: OpenApiDocumentField
×
UNCOV
109
    dependencies: OpenApiDocumentDependenciesField
×
110

111

UNCOV
112
class InferOpenApiDocumentDependenciesRequest(InferDependenciesRequest):
×
UNCOV
113
    infer_from = OpenApiDocumentDependenciesInferenceFieldSet
×
114

115

UNCOV
116
@rule
×
UNCOV
117
async def infer_openapi_document_dependencies(
×
118
    request: InferOpenApiDocumentDependenciesRequest,
119
) -> InferredDependencies:
120
    explicitly_provided_deps, hydrated_sources = await concurrently(
×
121
        determine_explicitly_provided_dependencies(
122
            **implicitly(DependenciesRequest(request.field_set.dependencies))
123
        ),
124
        hydrate_sources(HydrateSourcesRequest(request.field_set.sources), **implicitly()),
125
    )
126
    candidate_targets = await resolve_targets(
×
127
        **implicitly(
128
            RawSpecs(
129
                file_literals=(FileLiteralSpec(*hydrated_sources.snapshot.files),),
130
                description_of_origin="the `openapi_document` dependency inference",
131
            )
132
        )
133
    )
134

135
    addresses = frozenset(
×
136
        [target.address for target in candidate_targets if target.has_field(OpenApiSourceField)]
137
    )
138
    dependencies = explicitly_provided_deps.remaining_after_disambiguation(
×
139
        addresses.union(explicitly_provided_deps.includes),
140
        owners_must_be_ancestors=False,
141
    )
142

143
    return InferredDependencies(dependencies)
×
144

145

146
# -----------------------------------------------------------------------------------------------
147
# `openapi_source` dependency inference
148
# -----------------------------------------------------------------------------------------------
149

150

UNCOV
151
@dataclass(frozen=True)
×
UNCOV
152
class OpenApiSourceDependenciesInferenceFieldSet(FieldSet):
×
UNCOV
153
    required_fields = (OpenApiSourceField, OpenApiSourceDependenciesField)
×
154

UNCOV
155
    sources: OpenApiSourceField
×
UNCOV
156
    dependencies: OpenApiSourceDependenciesField
×
157

158

UNCOV
159
class InferOpenApiSourceDependenciesRequest(InferDependenciesRequest):
×
UNCOV
160
    infer_from = OpenApiSourceDependenciesInferenceFieldSet
×
161

162

UNCOV
163
@rule
×
UNCOV
164
async def infer_openapi_module_dependencies(
×
165
    request: InferOpenApiSourceDependenciesRequest,
166
) -> InferredDependencies:
167
    explicitly_provided_deps, hydrated_sources = await concurrently(
×
168
        determine_explicitly_provided_dependencies(
169
            **implicitly(DependenciesRequest(request.field_set.dependencies))
170
        ),
171
        hydrate_sources(HydrateSourcesRequest(request.field_set.sources), **implicitly()),
172
    )
173
    result = await parse_openapi_sources(
×
174
        ParseOpenApiSources(
175
            sources_digest=hydrated_sources.snapshot.digest,
176
            paths=hydrated_sources.snapshot.files,
177
        )
178
    )
179

180
    paths: set[str] = set()
×
181

182
    for source_file in hydrated_sources.snapshot.files:
×
183
        paths.update(result.dependencies[source_file])
×
184

185
    candidate_targets = await resolve_targets(
×
186
        **implicitly(
187
            RawSpecs(
188
                file_literals=tuple(FileLiteralSpec(path) for path in paths),
189
                unmatched_glob_behavior=GlobMatchErrorBehavior.ignore,
190
                description_of_origin="the `openapi_source` dependency inference",
191
            )
192
        )
193
    )
194

195
    addresses = frozenset(
×
196
        [target.address for target in candidate_targets if target.has_field(OpenApiSourceField)]
197
    )
198
    dependencies = explicitly_provided_deps.remaining_after_disambiguation(
×
199
        addresses.union(explicitly_provided_deps.includes),
200
        owners_must_be_ancestors=False,
201
    )
202

203
    return InferredDependencies(dependencies)
×
204

205

UNCOV
206
def rules():
×
UNCOV
207
    return [
×
208
        *collect_rules(),
209
        UnionRule(InferDependenciesRequest, InferOpenApiDocumentDependenciesRequest),
210
        UnionRule(InferDependenciesRequest, InferOpenApiSourceDependenciesRequest),
211
    ]
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