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

pantsbuild / pants / 22523112068

28 Feb 2026 03:01PM UTC coverage: 90.325% (-2.6%) from 92.93%
22523112068

push

github

web-flow
Prepare 2.32.0.dev3 (#23148)

82731 of 91593 relevant lines covered (90.32%)

3.28 hits per line

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

97.39
/src/python/pants/backend/python/dependency_inference/rules.py
1
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
9✔
5

6
import itertools
9✔
7
import logging
9✔
8
from collections.abc import Iterable
9✔
9
from dataclasses import dataclass
9✔
10
from enum import Enum
9✔
11
from pathlib import PurePath
9✔
12

13
from pants.backend.python.dependency_inference import module_mapper, parse_python_dependencies
9✔
14
from pants.backend.python.dependency_inference.default_unowned_dependencies import (
9✔
15
    DEFAULT_UNOWNED_DEPENDENCIES,
16
)
17
from pants.backend.python.dependency_inference.module_mapper import (
9✔
18
    PythonModuleOwners,
19
    PythonModuleOwnersRequest,
20
    ResolveName,
21
    map_module_to_address,
22
)
23
from pants.backend.python.dependency_inference.parse_python_dependencies import (
9✔
24
    ParsedPythonAssetPaths,
25
    ParsedPythonImports,
26
    ParsePythonDependenciesRequest,
27
    PythonFileDependencies,
28
)
29
from pants.backend.python.dependency_inference.parse_python_dependencies import (
9✔
30
    parse_python_dependencies as parse_python_dependencies_get,
31
)
32
from pants.backend.python.dependency_inference.subsystem import (
9✔
33
    AmbiguityResolution,
34
    InitFilesInference,
35
    PythonInferSubsystem,
36
)
37
from pants.backend.python.subsystems.setup import PythonSetup
9✔
38
from pants.backend.python.target_types import (
9✔
39
    InterpreterConstraintsField,
40
    PythonDependenciesField,
41
    PythonResolveField,
42
    PythonSourceField,
43
    PythonTestSourceField,
44
)
45
from pants.backend.python.util_rules import ancestor_files, pex
9✔
46
from pants.backend.python.util_rules.ancestor_files import AncestorFilesRequest, find_ancestor_files
9✔
47
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
9✔
48
from pants.core import target_types
9✔
49
from pants.core.target_types import AllAssetTargetsByPath, map_assets_by_path
9✔
50
from pants.core.util_rules import stripped_source_files
9✔
51
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
9✔
52
from pants.core.util_rules.unowned_dependency_behavior import (
9✔
53
    UnownedDependencyError,
54
    UnownedDependencyUsage,
55
)
56
from pants.engine.addresses import Address, Addresses
9✔
57
from pants.engine.internals.graph import (
9✔
58
    OwnersRequest,
59
    determine_explicitly_provided_dependencies,
60
    find_owners,
61
    resolve_targets,
62
)
63
from pants.engine.rules import concurrently, implicitly, rule
9✔
64
from pants.engine.target import (
9✔
65
    DependenciesRequest,
66
    ExplicitlyProvidedDependencies,
67
    FieldSet,
68
    InferDependenciesRequest,
69
    InferredDependencies,
70
)
71
from pants.engine.unions import UnionRule
9✔
72
from pants.source.source_root import SourceRootRequest, get_source_root
9✔
73
from pants.util.docutil import doc_url
9✔
74
from pants.util.strutil import bullet_list, softwrap
9✔
75

76
logger = logging.getLogger(__name__)
9✔
77

78

79
@dataclass(frozen=True)
9✔
80
class PythonImportDependenciesInferenceFieldSet(FieldSet):
9✔
81
    required_fields = (
9✔
82
        PythonSourceField,
83
        PythonDependenciesField,
84
        PythonResolveField,
85
        InterpreterConstraintsField,
86
    )
87

88
    source: PythonSourceField
9✔
89
    dependencies: PythonDependenciesField
9✔
90
    resolve: PythonResolveField
9✔
91
    interpreter_constraints: InterpreterConstraintsField
9✔
92

93

94
class InferPythonImportDependencies(InferDependenciesRequest):
9✔
95
    infer_from = PythonImportDependenciesInferenceFieldSet
9✔
96

97

98
def _get_inferred_asset_deps(
9✔
99
    address: Address,
100
    request_file_path: str,
101
    assets_by_path: AllAssetTargetsByPath,
102
    assets: ParsedPythonAssetPaths,
103
    explicitly_provided_deps: ExplicitlyProvidedDependencies,
104
) -> dict[str, ImportResolveResult]:
105
    def _resolve_single_asset(filepath) -> ImportResolveResult:
2✔
106
        # NB: Resources in Python's ecosystem are loaded relative to a package, so we only try and
107
        # query for a resource relative to requesting module's path
108
        # (I.e. we assume the user is doing something like `pkgutil.get_data(__file__, "foo/bar")`)
109
        # See https://docs.python.org/3/library/pkgutil.html#pkgutil.get_data
110
        # and Pants' own docs on resources.
111
        #
112
        # Files in Pants are always loaded relative to the build root without any source root
113
        # stripping, so we use the full filepath to query for files.
114
        # (I.e. we assume the user is doing something like `open("src/python/configs/prod.json")`)
115
        #
116
        # In either case we could also try and query based on the others' key, however this will
117
        # almost always lead to a false positive.
118
        resource_path = PurePath(request_file_path).parent / filepath
2✔
119
        file_path = PurePath(filepath)
2✔
120

121
        inferred_resource_tgts = assets_by_path.resources.get(resource_path, frozenset())
2✔
122
        inferred_file_tgts = assets_by_path.files.get(file_path, frozenset())
2✔
123
        inferred_tgts = inferred_resource_tgts | inferred_file_tgts
2✔
124

125
        if inferred_tgts:
2✔
126
            possible_addresses = tuple(tgt.address for tgt in inferred_tgts)
2✔
127
            if len(possible_addresses) == 1:
2✔
128
                return ImportResolveResult(ImportOwnerStatus.unambiguous, possible_addresses)
2✔
129

130
            explicitly_provided_deps.maybe_warn_of_ambiguous_dependency_inference(
1✔
131
                possible_addresses,
132
                address,
133
                import_reference="asset",
134
                context=f"The target {address} uses `{filepath}`",
135
            )
136
            maybe_disambiguated = explicitly_provided_deps.disambiguated(possible_addresses)
1✔
137
            if maybe_disambiguated:
1✔
138
                return ImportResolveResult(ImportOwnerStatus.disambiguated, (maybe_disambiguated,))
1✔
139
            else:
140
                return ImportResolveResult(ImportOwnerStatus.ambiguous)
1✔
141
        else:
142
            return ImportResolveResult(ImportOwnerStatus.unowned)
×
143

144
    return {filepath: _resolve_single_asset(filepath) for filepath in assets}
2✔
145

146

147
class ImportOwnerStatus(Enum):
9✔
148
    unambiguous = "unambiguous"
9✔
149
    disambiguated = "disambiguated"
9✔
150
    ambiguous = "ambiguous"
9✔
151
    unowned = "unowned"
9✔
152
    weak_ignore = "weak_ignore"
9✔
153
    unownable = "unownable"
9✔
154

155

156
@dataclass(frozen=True)
9✔
157
class ImportResolveResult:
9✔
158
    status: ImportOwnerStatus
9✔
159
    address: tuple[Address, ...] = ()
9✔
160

161

162
def _get_imports_info(
9✔
163
    address: Address,
164
    owners_per_import: Iterable[PythonModuleOwners],
165
    parsed_imports: ParsedPythonImports,
166
    explicitly_provided_deps: ExplicitlyProvidedDependencies,
167
) -> dict[str, ImportResolveResult]:
168
    def _resolve_single_import(owners, import_name) -> ImportResolveResult:
8✔
169
        if owners.unambiguous:
8✔
170
            return ImportResolveResult(ImportOwnerStatus.unambiguous, owners.unambiguous)
7✔
171

172
        explicitly_provided_deps.maybe_warn_of_ambiguous_dependency_inference(
6✔
173
            owners.ambiguous,
174
            address,
175
            import_reference="module",
176
            context=f"The target {address} imports `{import_name}`",
177
        )
178
        maybe_disambiguated = explicitly_provided_deps.disambiguated(owners.ambiguous)
6✔
179
        if maybe_disambiguated:
6✔
180
            return ImportResolveResult(ImportOwnerStatus.disambiguated, (maybe_disambiguated,))
1✔
181
        elif import_name.split(".")[0] in DEFAULT_UNOWNED_DEPENDENCIES:
6✔
182
            return ImportResolveResult(ImportOwnerStatus.unownable)
6✔
183
        elif parsed_imports[import_name].weak:
6✔
184
            return ImportResolveResult(ImportOwnerStatus.weak_ignore)
2✔
185
        else:
186
            return ImportResolveResult(ImportOwnerStatus.unowned)
6✔
187

188
    return {
8✔
189
        imp: _resolve_single_import(owners, imp)
190
        for owners, (imp, inf) in zip(owners_per_import, parsed_imports.items())
191
    }
192

193

194
def _collect_imports_info(
9✔
195
    resolve_result: dict[str, ImportResolveResult],
196
) -> tuple[frozenset[Address], frozenset[str]]:
197
    """Collect import resolution results into:
198

199
    - imports (direct and disambiguated)
200
    - unowned
201
    """
202

203
    return frozenset(
8✔
204
        addr
205
        for dep in resolve_result.values()
206
        for addr in dep.address
207
        if (
208
            dep.status == ImportOwnerStatus.unambiguous
209
            or dep.status == ImportOwnerStatus.disambiguated
210
        )
211
    ), frozenset(
212
        imp for imp, dep in resolve_result.items() if dep.status == ImportOwnerStatus.unowned
213
    )
214

215

216
def _remove_ignored_imports(
9✔
217
    unowned_imports: frozenset[str], ignored_paths: tuple[str, ...]
218
) -> frozenset[str]:
219
    """Remove unowned imports given a list of paths to ignore.
220

221
    E.g. having
222
    ```
223
    import foo.bar
224
    from foo.bar import baz
225
    import foo.barley
226
    ```
227

228
    and passing `ignored-paths=["foo.bar"]`, only `foo.bar` and `foo.bar.baz` will be ignored.
229
    """
230
    if not ignored_paths:
8✔
231
        return unowned_imports
8✔
232

233
    unowned_imports_filtered = set()
1✔
234
    for unowned_import in unowned_imports:
1✔
235
        if not any(
1✔
236
            unowned_import == ignored_path or unowned_import.startswith(f"{ignored_path}.")
237
            for ignored_path in ignored_paths
238
        ):
239
            unowned_imports_filtered.add(unowned_import)
1✔
240
    return frozenset(unowned_imports_filtered)
1✔
241

242

243
@dataclass(frozen=True)
9✔
244
class UnownedImportsPossibleOwnersRequest:
9✔
245
    """A request to find possible owners for several imports originating in a resolve."""
246

247
    unowned_imports: frozenset[str]
9✔
248
    original_resolve: str
9✔
249

250

251
@dataclass(frozen=True)
9✔
252
class UnownedImportPossibleOwnerRequest:
9✔
253
    unowned_import: str
9✔
254
    original_resolve: str
9✔
255

256

257
@dataclass(frozen=True)
9✔
258
class UnownedImportsPossibleOwners:
9✔
259
    value: dict[str, list[tuple[Address, ResolveName]]]
9✔
260

261

262
@dataclass(frozen=True)
9✔
263
class UnownedImportPossibleOwners:
9✔
264
    value: list[tuple[Address, ResolveName]]
9✔
265

266

267
async def _find_other_owners_for_unowned_imports(
9✔
268
    req: UnownedImportsPossibleOwnersRequest,
269
) -> UnownedImportsPossibleOwners:
270
    individual_possible_owners = await concurrently(
3✔
271
        find_other_owners_for_unowned_import(
272
            UnownedImportPossibleOwnerRequest(r, req.original_resolve), **implicitly()
273
        )
274
        for r in req.unowned_imports
275
    )
276

277
    return UnownedImportsPossibleOwners(
3✔
278
        {
279
            imported_module: possible_owners.value
280
            for imported_module, possible_owners in zip(
281
                req.unowned_imports, individual_possible_owners
282
            )
283
            if possible_owners.value
284
        }
285
    )
286

287

288
@rule
9✔
289
async def find_other_owners_for_unowned_import(
9✔
290
    req: UnownedImportPossibleOwnerRequest,
291
    python_setup: PythonSetup,
292
) -> UnownedImportPossibleOwners:
293
    other_owner_from_other_resolves = await map_module_to_address(
3✔
294
        PythonModuleOwnersRequest(req.unowned_import, resolve=None, locality=None), **implicitly()
295
    )
296

297
    owners = other_owner_from_other_resolves
3✔
298
    other_owners_as_targets = await resolve_targets(
3✔
299
        **implicitly(Addresses(owners.unambiguous + owners.ambiguous))
300
    )
301

302
    other_owners = []
3✔
303

304
    for t in other_owners_as_targets:
3✔
305
        other_owner_resolve = t[PythonResolveField].normalized_value(python_setup)
2✔
306
        if other_owner_resolve != req.original_resolve:
2✔
307
            other_owners.append((t.address, other_owner_resolve))
2✔
308
    return UnownedImportPossibleOwners(other_owners)
3✔
309

310

311
async def _handle_unowned_imports(
9✔
312
    address: Address,
313
    unowned_dependency_behavior: UnownedDependencyUsage,
314
    python_setup: PythonSetup,
315
    unowned_imports: frozenset[str],
316
    parsed_imports: ParsedPythonImports,
317
    resolve: str,
318
) -> None:
319
    if not unowned_imports or unowned_dependency_behavior is UnownedDependencyUsage.DoNothing:
8✔
320
        return
8✔
321

322
    other_resolves_snippet = ""
5✔
323
    if len(python_setup.resolves) > 1:
5✔
324
        imports_to_other_owners = (
2✔
325
            await _find_other_owners_for_unowned_imports(
326
                UnownedImportsPossibleOwnersRequest(unowned_imports, resolve),
327
            )
328
        ).value
329

330
        if imports_to_other_owners:
2✔
331
            other_resolves_lines = []
1✔
332
            for import_module, other_owners in sorted(imports_to_other_owners.items()):
1✔
333
                owners_txt = ", ".join(
1✔
334
                    f"'{other_resolve}' from {addr}" for addr, other_resolve in sorted(other_owners)
335
                )
336
                other_resolves_lines.append(f"{import_module}: {owners_txt}")
1✔
337
            other_resolves_snippet = "\n\n" + softwrap(
1✔
338
                f"""
339
                These imports are not in the resolve used by the target (`{resolve}`), but they
340
                were present in other resolves:
341

342
                {bullet_list(other_resolves_lines)}\n\n
343
                """
344
            )
345

346
    unowned_imports_with_lines = [
5✔
347
        f"{module_name} (line: {parsed_imports[module_name].lineno})"
348
        for module_name in sorted(unowned_imports)
349
    ]
350

351
    msg = softwrap(
5✔
352
        f"""
353
        Pants cannot infer owners for the following imports in the target {address}:
354

355
        {bullet_list(unowned_imports_with_lines)}{other_resolves_snippet}
356

357
        If you do not expect an import to be inferable, add `# pants: no-infer-dep` to the
358
        import line. Otherwise, see
359
        {doc_url("docs/using-pants/troubleshooting-common-issues#import-errors-and-missing-dependencies")} for common problems.
360
        """
361
    )
362
    if unowned_dependency_behavior is UnownedDependencyUsage.LogWarning:
5✔
363
        logger.warning(msg)
5✔
364
    else:
365
        raise UnownedDependencyError(msg)
1✔
366

367

368
async def _exec_parse_deps(
9✔
369
    field_set: PythonImportDependenciesInferenceFieldSet,
370
    python_setup: PythonSetup,
371
) -> PythonFileDependencies:
372
    source = await determine_source_files(SourceFilesRequest([field_set.source]))
8✔
373
    resp = await parse_python_dependencies_get(
8✔
374
        ParsePythonDependenciesRequest(
375
            source,
376
        ),
377
        **implicitly(),
378
    )
379
    assert len(resp.path_to_deps) == 1
8✔
380
    return next(iter(resp.path_to_deps.values()))
8✔
381

382

383
@dataclass(frozen=True)
9✔
384
class ResolvedParsedPythonDependenciesRequest:
9✔
385
    field_set: PythonImportDependenciesInferenceFieldSet
9✔
386
    parsed_dependencies: PythonFileDependencies
9✔
387
    resolve: str | None
9✔
388

389

390
@dataclass(frozen=True)
9✔
391
class ResolvedParsedPythonDependencies:
9✔
392
    resolve_results: dict[str, ImportResolveResult]
9✔
393
    assets: dict[str, ImportResolveResult]
9✔
394
    explicit: ExplicitlyProvidedDependencies
9✔
395

396

397
@rule
9✔
398
async def resolve_parsed_dependencies(
9✔
399
    request: ResolvedParsedPythonDependenciesRequest,
400
    python_infer_subsystem: PythonInferSubsystem,
401
) -> ResolvedParsedPythonDependencies:
402
    """Find the owning targets for the parsed dependencies."""
403

404
    parsed_imports = request.parsed_dependencies.imports
8✔
405
    parsed_assets = request.parsed_dependencies.assets
8✔
406
    if not python_infer_subsystem.imports:
8✔
407
        parsed_imports = ParsedPythonImports([])
×
408

409
    explicitly_provided_deps = await determine_explicitly_provided_dependencies(
8✔
410
        **implicitly(DependenciesRequest(request.field_set.dependencies))
411
    )
412

413
    # Only set locality if needed, to avoid unnecessary rule graph memoization misses.
414
    # When set, use the source root, which is useful in practice, but incurs fewer memoization
415
    # misses than using the full spec_path.
416
    locality = None
8✔
417
    if python_infer_subsystem.ambiguity_resolution == AmbiguityResolution.by_source_root:
8✔
418
        source_root = await get_source_root(
×
419
            SourceRootRequest.for_address(request.field_set.address)
420
        )
421
        locality = source_root.path
×
422

423
    if parsed_imports:
8✔
424
        owners_per_import = await concurrently(
8✔
425
            map_module_to_address(
426
                PythonModuleOwnersRequest(imported_module, request.resolve, locality),
427
                **implicitly(),
428
            )
429
            for imported_module in parsed_imports
430
        )
431
        resolve_results = _get_imports_info(
8✔
432
            address=request.field_set.address,
433
            owners_per_import=owners_per_import,
434
            parsed_imports=parsed_imports,
435
            explicitly_provided_deps=explicitly_provided_deps,
436
        )
437
    else:
438
        resolve_results = {}
8✔
439

440
    if parsed_assets:
8✔
441
        assets_by_path = await map_assets_by_path(**implicitly())
2✔
442
        asset_deps = _get_inferred_asset_deps(
2✔
443
            request.field_set.address,
444
            request.field_set.source.file_path,
445
            assets_by_path,
446
            parsed_assets,
447
            explicitly_provided_deps,
448
        )
449
    else:
450
        asset_deps = {}
8✔
451

452
    return ResolvedParsedPythonDependencies(
8✔
453
        resolve_results=resolve_results,
454
        assets=asset_deps,
455
        explicit=explicitly_provided_deps,
456
    )
457

458

459
@rule(desc="Inferring Python dependencies by analyzing source")
9✔
460
async def infer_python_dependencies_via_source(
9✔
461
    request: InferPythonImportDependencies,
462
    python_infer_subsystem: PythonInferSubsystem,
463
    python_setup: PythonSetup,
464
) -> InferredDependencies:
465
    if not python_infer_subsystem.imports and not python_infer_subsystem.assets:
8✔
466
        return InferredDependencies([])
×
467

468
    parsed_dependencies = await _exec_parse_deps(request.field_set, python_setup)
8✔
469

470
    resolve = request.field_set.resolve.normalized_value(python_setup)
8✔
471

472
    resolved_dependencies = await resolve_parsed_dependencies(
8✔
473
        ResolvedParsedPythonDependenciesRequest(request.field_set, parsed_dependencies, resolve),
474
        **implicitly(),
475
    )
476
    import_deps, unowned_imports = _collect_imports_info(resolved_dependencies.resolve_results)
8✔
477
    unowned_imports = _remove_ignored_imports(
8✔
478
        unowned_imports, python_infer_subsystem.ignored_unowned_imports
479
    )
480

481
    asset_deps, unowned_assets = _collect_imports_info(resolved_dependencies.assets)
8✔
482

483
    inferred_deps = import_deps | asset_deps
8✔
484

485
    await _handle_unowned_imports(
8✔
486
        request.field_set.address,
487
        python_infer_subsystem.unowned_dependency_behavior,
488
        python_setup,
489
        unowned_imports,
490
        parsed_dependencies.imports,
491
        resolve=resolve,
492
    )
493

494
    return InferredDependencies(sorted(inferred_deps))
8✔
495

496

497
@dataclass(frozen=True)
9✔
498
class InitDependenciesInferenceFieldSet(FieldSet):
9✔
499
    required_fields = (PythonSourceField, PythonResolveField)
9✔
500

501
    source: PythonSourceField
9✔
502
    resolve: PythonResolveField
9✔
503

504

505
class InferInitDependencies(InferDependenciesRequest):
9✔
506
    infer_from = InitDependenciesInferenceFieldSet
9✔
507

508

509
@rule(desc="Inferring dependencies on `__init__.py` files")
9✔
510
async def infer_python_init_dependencies(
9✔
511
    request: InferInitDependencies,
512
    python_infer_subsystem: PythonInferSubsystem,
513
    python_setup: PythonSetup,
514
) -> InferredDependencies:
515
    if python_infer_subsystem.init_files is InitFilesInference.never:
5✔
516
        return InferredDependencies([])
1✔
517

518
    ignore_empty_files = python_infer_subsystem.init_files is InitFilesInference.content_only
5✔
519
    fp = request.field_set.source.file_path
5✔
520
    assert fp is not None
5✔
521
    init_files = await find_ancestor_files(
5✔
522
        AncestorFilesRequest(
523
            input_files=(fp,),
524
            requested=("__init__.py", "__init__.pyi"),
525
            ignore_empty_files=ignore_empty_files,
526
        )
527
    )
528
    owners = await concurrently(
5✔
529
        find_owners(OwnersRequest((f,)), **implicitly()) for f in init_files.snapshot.files
530
    )
531

532
    owner_tgts = await resolve_targets(
5✔
533
        **implicitly(Addresses(itertools.chain.from_iterable(owners)))
534
    )
535
    resolve = request.field_set.resolve.normalized_value(python_setup)
5✔
536
    python_owners = [
5✔
537
        tgt.address
538
        for tgt in owner_tgts
539
        if (
540
            tgt.has_field(PythonSourceField)
541
            and tgt[PythonResolveField].normalized_value(python_setup) == resolve
542
        )
543
    ]
544
    return InferredDependencies(python_owners)
5✔
545

546

547
@dataclass(frozen=True)
9✔
548
class ConftestDependenciesInferenceFieldSet(FieldSet):
9✔
549
    required_fields = (PythonTestSourceField, PythonResolveField)
9✔
550

551
    source: PythonTestSourceField
9✔
552
    resolve: PythonResolveField
9✔
553

554

555
class InferConftestDependencies(InferDependenciesRequest):
9✔
556
    infer_from = ConftestDependenciesInferenceFieldSet
9✔
557

558

559
@rule(desc="Inferring dependencies on `conftest.py` files")
9✔
560
async def infer_python_conftest_dependencies(
9✔
561
    request: InferConftestDependencies,
562
    python_infer_subsystem: PythonInferSubsystem,
563
    python_setup: PythonSetup,
564
) -> InferredDependencies:
565
    if not python_infer_subsystem.conftests:
3✔
566
        return InferredDependencies([])
×
567

568
    fp = request.field_set.source.file_path
3✔
569
    assert fp is not None
3✔
570
    conftest_files = await find_ancestor_files(
3✔
571
        AncestorFilesRequest(input_files=(fp,), requested=("conftest.py",))
572
    )
573
    owners = await concurrently(
3✔
574
        # NB: Because conftest.py files effectively always have content, we require an
575
        # owning target.
576
        find_owners(
577
            OwnersRequest((f,), owners_not_found_behavior=GlobMatchErrorBehavior.error),
578
            **implicitly(),
579
        )
580
        for f in conftest_files.snapshot.files
581
    )
582

583
    owner_tgts = await resolve_targets(
3✔
584
        **implicitly(Addresses(itertools.chain.from_iterable(owners)))
585
    )
586
    resolve = request.field_set.resolve.normalized_value(python_setup)
3✔
587
    python_owners = [
3✔
588
        tgt.address
589
        for tgt in owner_tgts
590
        if (
591
            tgt.has_field(PythonSourceField)
592
            and tgt[PythonResolveField].normalized_value(python_setup) == resolve
593
        )
594
    ]
595
    return InferredDependencies(python_owners)
3✔
596

597

598
# This is a separate function to facilitate tests registering import inference.
599
def import_rules():
9✔
600
    return [
9✔
601
        resolve_parsed_dependencies,
602
        find_other_owners_for_unowned_import,
603
        infer_python_dependencies_via_source,
604
        *pex.rules(),
605
        *parse_python_dependencies.rules(),
606
        *module_mapper.rules(),
607
        *stripped_source_files.rules(),
608
        *target_types.rules(),
609
        *PythonInferSubsystem.rules(),
610
        *PythonSetup.rules(),
611
        UnionRule(InferDependenciesRequest, InferPythonImportDependencies),
612
    ]
613

614

615
def rules():
9✔
616
    return [
7✔
617
        *import_rules(),
618
        infer_python_init_dependencies,
619
        infer_python_conftest_dependencies,
620
        *ancestor_files.rules(),
621
        UnionRule(InferDependenciesRequest, InferInitDependencies),
622
        UnionRule(InferDependenciesRequest, InferConftestDependencies),
623
    ]
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