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

pantsbuild / pants / 21538902371

31 Jan 2026 04:39AM UTC coverage: 80.332% (+0.001%) from 80.331%
21538902371

Pull #23060

github

web-flow
Merge b2da87474 into 89e424102
Pull Request #23060: Simplify API of Python dep inference.

6 of 8 new or added lines in 3 files covered. (75.0%)

7 existing lines in 3 files now uncovered.

78561 of 97796 relevant lines covered (80.33%)

3.36 hits per line

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

54.11
/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
12✔
5

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

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

77
logger = logging.getLogger(__name__)
12✔
78

79

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

89
    source: PythonSourceField
12✔
90
    dependencies: PythonDependenciesField
12✔
91
    resolve: PythonResolveField
12✔
92
    interpreter_constraints: InterpreterConstraintsField
12✔
93

94

95
class InferPythonImportDependencies(InferDependenciesRequest):
12✔
96
    infer_from = PythonImportDependenciesInferenceFieldSet
12✔
97

98

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

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

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

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

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

147

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

156

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

162

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

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

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

194

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

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

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

216

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

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

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

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

243

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

248
    unowned_imports: frozenset[str]
12✔
249
    original_resolve: str
12✔
250

251

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

257

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

262

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

267

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

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

288

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

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

303
    other_owners = []
×
304

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

311

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

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

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

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

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

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

356
        {bullet_list(unowned_imports_with_lines)}{other_resolves_snippet}
357

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

368

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

386

387
@dataclass(frozen=True)
12✔
388
class ResolvedParsedPythonDependenciesRequest:
12✔
389
    field_set: PythonImportDependenciesInferenceFieldSet
12✔
390
    parsed_dependencies: ParsedPythonDependencies
12✔
391
    resolve: str | None
12✔
392

393

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

400

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

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

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

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

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

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

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

462

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

472
    parsed_dependencies = await _exec_parse_deps(request.field_set, python_setup)
×
473

474
    resolve = request.field_set.resolve.normalized_value(python_setup)
×
475

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

485
    asset_deps, unowned_assets = _collect_imports_info(resolved_dependencies.assets)
×
486

487
    inferred_deps = import_deps | asset_deps
×
488

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

498
    return InferredDependencies(sorted(inferred_deps))
×
499

500

501
@dataclass(frozen=True)
12✔
502
class InitDependenciesInferenceFieldSet(FieldSet):
12✔
503
    required_fields = (PythonSourceField, PythonResolveField)
12✔
504

505
    source: PythonSourceField
12✔
506
    resolve: PythonResolveField
12✔
507

508

509
class InferInitDependencies(InferDependenciesRequest):
12✔
510
    infer_from = InitDependenciesInferenceFieldSet
12✔
511

512

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

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

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

550

551
@dataclass(frozen=True)
12✔
552
class ConftestDependenciesInferenceFieldSet(FieldSet):
12✔
553
    required_fields = (PythonTestSourceField, PythonResolveField)
12✔
554

555
    source: PythonTestSourceField
12✔
556
    resolve: PythonResolveField
12✔
557

558

559
class InferConftestDependencies(InferDependenciesRequest):
12✔
560
    infer_from = ConftestDependenciesInferenceFieldSet
12✔
561

562

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

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

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

601

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

618

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