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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

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

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

UNCOV
4
import json
×
UNCOV
5
from pathlib import PurePath
×
6

UNCOV
7
from pants.backend.python import dependency_inference
×
UNCOV
8
from pants.backend.python.dependency_inference.parse_python_dependencies import (
×
9
    ExplicitPythonDependencies,
10
    ParsedPythonAssetPaths,
11
    ParsedPythonImportInfo,
12
    ParsedPythonImports,
13
    PythonFileDependencies,
14
)
UNCOV
15
from pants.backend.python.dependency_inference.rules import (
×
16
    ImportOwnerStatus,
17
    PythonImportDependenciesInferenceFieldSet,
18
    ResolvedParsedPythonDependenciesRequest,
19
    resolve_parsed_dependencies,
20
)
UNCOV
21
from pants.backend.python.framework.django.detect_apps import DjangoApps
×
UNCOV
22
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
23
from pants.backend.python.target_types import EntryPoint
×
UNCOV
24
from pants.backend.python.util_rules import pex
×
UNCOV
25
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
UNCOV
26
from pants.backend.python.util_rules.pex import PexRequest, VenvPexProcess, create_venv_pex
×
UNCOV
27
from pants.base.specs import FileGlobSpec, RawSpecs
×
UNCOV
28
from pants.core.util_rules.source_files import SourceFilesRequest
×
UNCOV
29
from pants.core.util_rules.stripped_source_files import strip_source_roots
×
UNCOV
30
from pants.engine.fs import CreateDigest, FileContent
×
UNCOV
31
from pants.engine.internals.graph import resolve_targets
×
UNCOV
32
from pants.engine.intrinsics import create_digest
×
UNCOV
33
from pants.engine.process import execute_process_or_raise
×
UNCOV
34
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
35
from pants.engine.target import InferDependenciesRequest, InferredDependencies
×
UNCOV
36
from pants.engine.unions import UnionRule
×
UNCOV
37
from pants.util.logging import LogLevel
×
UNCOV
38
from pants.util.resources import read_resource
×
39

40

UNCOV
41
class InferDjangoDependencies(InferDependenciesRequest):
×
UNCOV
42
    infer_from = PythonImportDependenciesInferenceFieldSet
×
43

44

UNCOV
45
_visitor_resource = "scripts/dependency_visitor.py"
×
UNCOV
46
_implicit_dependency_packages = (
×
47
    "management.commands",
48
    "migrations",
49
)
50

51

UNCOV
52
async def _django_migration_dependencies(
×
53
    request: InferDjangoDependencies,
54
    python_setup: PythonSetup,
55
    django_apps: DjangoApps,
56
) -> InferredDependencies:
UNCOV
57
    stripped_sources = await strip_source_roots(
×
58
        **implicitly(SourceFilesRequest([request.field_set.source]))
59
    )
UNCOV
60
    assert len(stripped_sources.snapshot.files) == 1
×
61

UNCOV
62
    file_content = FileContent("__visitor.py", read_resource(__name__, _visitor_resource))
×
UNCOV
63
    visitor_digest = await create_digest(CreateDigest([file_content]))
×
UNCOV
64
    venv_pex = await create_venv_pex(
×
65
        **implicitly(
66
            PexRequest(
67
                output_filename="__visitor.pex",
68
                internal_only=True,
69
                main=EntryPoint("__visitor"),
70
                interpreter_constraints=InterpreterConstraints.create_from_field_sets(
71
                    [request.field_set], python_setup=python_setup
72
                ),
73
                sources=visitor_digest,
74
            )
75
        )
76
    )
UNCOV
77
    process_result = await execute_process_or_raise(
×
78
        **implicitly(
79
            VenvPexProcess(
80
                venv_pex,
81
                argv=[stripped_sources.snapshot.files[0]],
82
                description=f"Determine Django app dependencies for {request.field_set.address}",
83
                input_digest=stripped_sources.snapshot.digest,
84
                level=LogLevel.DEBUG,
85
            )
86
        )
87
    )
88
    # See in script for where we explicitly encoded as utf8. Even though utf8 is the
89
    # default for decode(), we make that explicit here for emphasis.
UNCOV
90
    process_output = process_result.stdout.decode("utf8") or "{}"
×
UNCOV
91
    modules = [
×
92
        f"{django_apps.label_to_name[label]}.migrations.{migration}"
93
        for label, migration in json.loads(process_output)
94
        if label in django_apps.label_to_name
95
    ]
UNCOV
96
    resolve = request.field_set.resolve.normalized_value(python_setup)
×
97

UNCOV
98
    resolved_dependencies = await resolve_parsed_dependencies(
×
99
        ResolvedParsedPythonDependenciesRequest(
100
            request.field_set,
101
            PythonFileDependencies(
102
                ParsedPythonImports(
103
                    (module, ParsedPythonImportInfo(0, False)) for module in modules
104
                ),
105
                ParsedPythonAssetPaths(),
106
                ExplicitPythonDependencies(),
107
            ),
108
            resolve,
109
        ),
110
        **implicitly(),
111
    )
112

UNCOV
113
    return InferredDependencies(
×
114
        sorted(
115
            address
116
            for result in resolved_dependencies.resolve_results.values()
117
            if result.status in (ImportOwnerStatus.unambiguous, ImportOwnerStatus.disambiguated)
118
            for address in result.address
119
        )
120
    )
121

122

UNCOV
123
async def _django_app_implicit_dependencies(
×
124
    request: InferDjangoDependencies,
125
    python_setup: PythonSetup,
126
    django_apps: DjangoApps,
127
) -> InferredDependencies:
UNCOV
128
    file_path = request.field_set.source.file_path
×
UNCOV
129
    apps = [
×
130
        django_app for django_app in django_apps.values() if django_app.config_file == file_path
131
    ]
UNCOV
132
    if not apps:
×
133
        return InferredDependencies([])
×
134

UNCOV
135
    app_package = apps[0].name
×
136

UNCOV
137
    implicit_dependency_packages = [
×
138
        f"{app_package}.{subpackage}" for subpackage in _implicit_dependency_packages
139
    ]
140

UNCOV
141
    resolve = request.field_set.resolve.normalized_value(python_setup)
×
142

UNCOV
143
    resolved_dependencies = await resolve_parsed_dependencies(
×
144
        ResolvedParsedPythonDependenciesRequest(
145
            request.field_set,
146
            PythonFileDependencies(
147
                ParsedPythonImports(
148
                    (package, ParsedPythonImportInfo(0, False))
149
                    for package in implicit_dependency_packages
150
                ),
151
                ParsedPythonAssetPaths(),
152
                ExplicitPythonDependencies(),
153
            ),
154
            resolve,
155
        ),
156
        **implicitly(),
157
    )
158

UNCOV
159
    spec_paths = [
×
160
        address.spec_path
161
        for result in resolved_dependencies.resolve_results.values()
162
        if result.status in (ImportOwnerStatus.unambiguous, ImportOwnerStatus.disambiguated)
163
        for address in result.address
164
    ]
165

UNCOV
166
    targets = await resolve_targets(
×
167
        **implicitly(
168
            RawSpecs.create(
169
                specs=[FileGlobSpec(f"{spec_path}/*.py") for spec_path in spec_paths],
170
                description_of_origin="Django implicit dependency detection",
171
            )
172
        )
173
    )
174

UNCOV
175
    return InferredDependencies(sorted(target.address for target in targets))
×
176

177

UNCOV
178
@rule
×
UNCOV
179
async def infer_django_dependencies(
×
180
    request: InferDjangoDependencies,
181
    python_setup: PythonSetup,
182
    django_apps: DjangoApps,
183
) -> InferredDependencies:
UNCOV
184
    source_field = request.field_set.source
×
185
    # NB: This doesn't consider https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-MIGRATION_MODULES
UNCOV
186
    path = PurePath(source_field.file_path)
×
UNCOV
187
    if path.match("migrations/*.py"):
×
UNCOV
188
        return await _django_migration_dependencies(request, python_setup, django_apps)
×
UNCOV
189
    elif path.match("apps.py"):
×
UNCOV
190
        return await _django_app_implicit_dependencies(request, python_setup, django_apps)
×
191
    else:
192
        return InferredDependencies([])
×
193

194

UNCOV
195
def rules():
×
UNCOV
196
    return (
×
197
        *collect_rules(),
198
        *pex.rules(),
199
        *dependency_inference.rules.rules(),
200
        UnionRule(InferDependenciesRequest, InferDjangoDependencies),
201
    )
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