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

pantsbuild / pants / 19250292619

11 Nov 2025 12:09AM UTC coverage: 77.865% (-2.4%) from 80.298%
19250292619

push

github

web-flow
flag non-runnable targets used with `code_quality_tool` (#22875)

2 of 5 new or added lines in 2 files covered. (40.0%)

1487 existing lines in 72 files now uncovered.

71448 of 91759 relevant lines covered (77.86%)

3.22 hits per line

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

48.47
/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
11✔
5

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

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

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

78

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

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

93

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

97

98
def _get_inferred_asset_deps(
11✔
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:
×
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
×
119
        file_path = PurePath(filepath)
×
120

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

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

130
            explicitly_provided_deps.maybe_warn_of_ambiguous_dependency_inference(
×
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)
×
137
            if maybe_disambiguated:
×
138
                return ImportResolveResult(ImportOwnerStatus.disambiguated, (maybe_disambiguated,))
×
139
            else:
140
                return ImportResolveResult(ImportOwnerStatus.ambiguous)
×
141
        else:
142
            return ImportResolveResult(ImportOwnerStatus.unowned)
×
143

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

146

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

155

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

161

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

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

UNCOV
188
    return {
×
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(
11✔
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(
×
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(
11✔
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:
×
231
        return unowned_imports
×
232

233
    unowned_imports_filtered = set()
×
234
    for unowned_import in unowned_imports:
×
235
        if not any(
×
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)
×
240
    return frozenset(unowned_imports_filtered)
×
241

242

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

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

250

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

256

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

261

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

266

267
async def _find_other_owners_for_unowned_imports(
11✔
268
    req: UnownedImportsPossibleOwnersRequest,
269
) -> UnownedImportsPossibleOwners:
270
    individual_possible_owners = await concurrently(
×
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(
×
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
11✔
289
async def find_other_owners_for_unowned_import(
11✔
290
    req: UnownedImportPossibleOwnerRequest,
291
    python_setup: PythonSetup,
292
) -> UnownedImportPossibleOwners:
293
    other_owner_from_other_resolves = await map_module_to_address(
×
294
        PythonModuleOwnersRequest(req.unowned_import, resolve=None, locality=None), **implicitly()
295
    )
296

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

302
    other_owners = []
×
303

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

310

311
async def _handle_unowned_imports(
11✔
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:
×
320
        return
×
321

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

330
        if imports_to_other_owners:
×
331
            other_resolves_lines = []
×
332
            for import_module, other_owners in sorted(imports_to_other_owners.items()):
×
333
                owners_txt = ", ".join(
×
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}")
×
337
            other_resolves_snippet = "\n\n" + softwrap(
×
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 = [
×
347
        f"{module_name} (line: {parsed_imports[module_name].lineno})"
348
        for module_name in sorted(unowned_imports)
349
    ]
350

351
    msg = softwrap(
×
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:
×
363
        logger.warning(msg)
×
364
    else:
365
        raise UnownedDependencyError(msg)
×
366

367

368
async def _exec_parse_deps(
11✔
369
    field_set: PythonImportDependenciesInferenceFieldSet,
370
    python_setup: PythonSetup,
371
) -> ParsedPythonDependencies:
372
    interpreter_constraints = InterpreterConstraints.create_from_field_sets(
×
373
        [field_set], python_setup
374
    )
375
    resp = await parse_python_dependencies_get(
×
376
        ParsePythonDependenciesRequest(
377
            field_set.source,
378
            interpreter_constraints,
379
        ),
380
        **implicitly(),
381
    )
382
    return resp
×
383

384

385
@dataclass(frozen=True)
11✔
386
class ResolvedParsedPythonDependenciesRequest:
11✔
387
    field_set: PythonImportDependenciesInferenceFieldSet
11✔
388
    parsed_dependencies: ParsedPythonDependencies
11✔
389
    resolve: str | None
11✔
390

391

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

398

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

406
    parsed_imports = request.parsed_dependencies.imports
×
407
    parsed_assets = request.parsed_dependencies.assets
×
408
    if not python_infer_subsystem.imports:
×
409
        parsed_imports = ParsedPythonImports([])
×
410

411
    explicitly_provided_deps = await determine_explicitly_provided_dependencies(
×
412
        **implicitly(DependenciesRequest(request.field_set.dependencies))
413
    )
414

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

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

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

454
    return ResolvedParsedPythonDependencies(
×
455
        resolve_results=resolve_results,
456
        assets=asset_deps,
457
        explicit=explicitly_provided_deps,
458
    )
459

460

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

470
    parsed_dependencies = await _exec_parse_deps(request.field_set, python_setup)
×
471

472
    resolve = request.field_set.resolve.normalized_value(python_setup)
×
473

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

483
    asset_deps, unowned_assets = _collect_imports_info(resolved_dependencies.assets)
×
484

485
    inferred_deps = import_deps | asset_deps
×
486

487
    await _handle_unowned_imports(
×
488
        request.field_set.address,
489
        python_infer_subsystem.unowned_dependency_behavior,
490
        python_setup,
491
        unowned_imports,
492
        parsed_dependencies.imports,
493
        resolve=resolve,
494
    )
495

496
    return InferredDependencies(sorted(inferred_deps))
×
497

498

499
@dataclass(frozen=True)
11✔
500
class InitDependenciesInferenceFieldSet(FieldSet):
11✔
501
    required_fields = (PythonSourceField, PythonResolveField)
11✔
502

503
    source: PythonSourceField
11✔
504
    resolve: PythonResolveField
11✔
505

506

507
class InferInitDependencies(InferDependenciesRequest):
11✔
508
    infer_from = InitDependenciesInferenceFieldSet
11✔
509

510

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

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

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

548

549
@dataclass(frozen=True)
11✔
550
class ConftestDependenciesInferenceFieldSet(FieldSet):
11✔
551
    required_fields = (PythonTestSourceField, PythonResolveField)
11✔
552

553
    source: PythonTestSourceField
11✔
554
    resolve: PythonResolveField
11✔
555

556

557
class InferConftestDependencies(InferDependenciesRequest):
11✔
558
    infer_from = ConftestDependenciesInferenceFieldSet
11✔
559

560

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

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

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

599

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

616

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