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

pantsbuild / pants / 18987624565

31 Oct 2025 11:28PM UTC coverage: 80.299% (+0.02%) from 80.275%
18987624565

push

github

web-flow
Allow setting Python resolve interpreter_constraints as defaults for targets (#22676)

Closes #22574 

This PR is intended to solve a long-felt annoyance of mine when working
in repos with multiple Python resolves, which is having to configure
resolve interpreter constraints and source interpreter constraints
separately. It adds a new option,
`[python].default_to_resolve_interpreter_constraints`, which when set to
true, tells Pants to use the interpreter constraints of the resolve,
rather than the global interpreter constraints, if no interpreter
constraints are provided. If resolves are not enabled or no interpreter
constraints are set for the resolve, it still falls back to the global
default.

71 of 95 new or added lines in 17 files covered. (74.74%)

2 existing lines in 2 files now uncovered.

77993 of 97128 relevant lines covered (80.3%)

3.35 hits per line

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

54.15
/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.unowned_dependency_behavior import (
12✔
53
    UnownedDependencyError,
54
    UnownedDependencyUsage,
55
)
56
from pants.engine.addresses import Address, Addresses
12✔
57
from pants.engine.internals.graph import (
12✔
58
    OwnersRequest,
59
    determine_explicitly_provided_dependencies,
60
    find_owners,
61
    resolve_targets,
62
)
63
from pants.engine.rules import concurrently, implicitly, rule
12✔
64
from pants.engine.target import (
12✔
65
    DependenciesRequest,
66
    ExplicitlyProvidedDependencies,
67
    FieldSet,
68
    InferDependenciesRequest,
69
    InferredDependencies,
70
)
71
from pants.engine.unions import UnionRule
12✔
72
from pants.source.source_root import SourceRootRequest, get_source_root
12✔
73
from pants.util.docutil import doc_url
12✔
74
from pants.util.strutil import bullet_list, softwrap
12✔
75

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

78

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

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

93

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

97

98
def _get_inferred_asset_deps(
12✔
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):
12✔
148
    unambiguous = "unambiguous"
12✔
149
    disambiguated = "disambiguated"
12✔
150
    ambiguous = "ambiguous"
12✔
151
    unowned = "unowned"
12✔
152
    weak_ignore = "weak_ignore"
12✔
153
    unownable = "unownable"
12✔
154

155

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

161

162
def _get_imports_info(
12✔
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:
1✔
169
        if owners.unambiguous:
1✔
170
            return ImportResolveResult(ImportOwnerStatus.unambiguous, owners.unambiguous)
1✔
171

172
        explicitly_provided_deps.maybe_warn_of_ambiguous_dependency_inference(
1✔
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)
1✔
179
        if maybe_disambiguated:
1✔
180
            return ImportResolveResult(ImportOwnerStatus.disambiguated, (maybe_disambiguated,))
1✔
181
        elif import_name.split(".")[0] in DEFAULT_UNOWNED_DEPENDENCIES:
1✔
182
            return ImportResolveResult(ImportOwnerStatus.unownable)
1✔
183
        elif parsed_imports[import_name].weak:
1✔
184
            return ImportResolveResult(ImportOwnerStatus.weak_ignore)
1✔
185
        else:
186
            return ImportResolveResult(ImportOwnerStatus.unowned)
1✔
187

188
    return {
1✔
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(
12✔
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(
12✔
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)
12✔
244
class UnownedImportsPossibleOwnersRequest:
12✔
245
    """A request to find possible owners for several imports originating in a resolve."""
246

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

250

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

256

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

261

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

266

267
async def _find_other_owners_for_unowned_imports(
12✔
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
12✔
289
async def find_other_owners_for_unowned_import(
12✔
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(
12✔
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(
12✔
369
    field_set: PythonImportDependenciesInferenceFieldSet,
370
    python_setup: PythonSetup,
371
) -> ParsedPythonDependencies:
NEW
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)
12✔
386
class ResolvedParsedPythonDependenciesRequest:
12✔
387
    field_set: PythonImportDependenciesInferenceFieldSet
12✔
388
    parsed_dependencies: ParsedPythonDependencies
12✔
389
    resolve: str | None
12✔
390

391

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

398

399
@rule
12✔
400
async def resolve_parsed_dependencies(
12✔
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")
12✔
462
async def infer_python_dependencies_via_source(
12✔
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)
12✔
500
class InitDependenciesInferenceFieldSet(FieldSet):
12✔
501
    required_fields = (PythonSourceField, PythonResolveField)
12✔
502

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

506

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

510

511
@rule(desc="Inferring dependencies on `__init__.py` files")
12✔
512
async def infer_python_init_dependencies(
12✔
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)
12✔
550
class ConftestDependenciesInferenceFieldSet(FieldSet):
12✔
551
    required_fields = (PythonTestSourceField, PythonResolveField)
12✔
552

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

556

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

560

561
@rule(desc="Inferring dependencies on `conftest.py` files")
12✔
562
async def infer_python_conftest_dependencies(
12✔
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():
12✔
602
    return [
12✔
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():
12✔
618
    return [
9✔
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