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

pantsbuild / pants / 18252174847

05 Oct 2025 01:36AM UTC coverage: 43.382% (-36.9%) from 80.261%
18252174847

push

github

web-flow
run tests on mac arm (#22717)

Just doing the minimal to pull forward the x86_64 pattern.

ref #20993

25776 of 59416 relevant lines covered (43.38%)

1.3 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).
3
from __future__ import annotations
×
4

5
import contextlib
×
6
import json
×
7
import os.path
×
8
from collections.abc import Mapping
×
9
from dataclasses import dataclass
×
10
from typing import Any
×
11

12
import yaml
×
13

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

42

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

48

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

53

54
@rule
×
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

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

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

108
    sources: OpenApiDocumentField
×
109
    dependencies: OpenApiDocumentDependenciesField
×
110

111

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

115

116
@rule
×
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

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

155
    sources: OpenApiSourceField
×
156
    dependencies: OpenApiSourceDependenciesField
×
157

158

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

162

163
@rule
×
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

206
def rules():
×
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