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

pantsbuild / pants / 22740642519

05 Mar 2026 11:00PM UTC coverage: 52.677% (-40.3%) from 92.931%
22740642519

Pull #23157

github

web-flow
Merge 2aa18e6d4 into f0030f5e7
Pull Request #23157: [pants ng] Partition source files by config.

31678 of 60136 relevant lines covered (52.68%)

0.53 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

4
import json
×
5
from pathlib import PurePath
×
6

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

40

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

44

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

51

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

62
    file_content = FileContent("__visitor.py", read_resource(__name__, _visitor_resource))
×
63
    visitor_digest = await create_digest(CreateDigest([file_content]))
×
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
    )
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.
90
    process_output = process_result.stdout.decode("utf8") or "{}"
×
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
    ]
96
    resolve = request.field_set.resolve.normalized_value(python_setup)
×
97

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

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

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

135
    app_package = apps[0].name
×
136

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

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

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

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

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

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

177

178
@rule
×
179
async def infer_django_dependencies(
×
180
    request: InferDjangoDependencies,
181
    python_setup: PythonSetup,
182
    django_apps: DjangoApps,
183
) -> InferredDependencies:
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
186
    path = PurePath(source_field.file_path)
×
187
    if path.match("migrations/*.py"):
×
188
        return await _django_migration_dependencies(request, python_setup, django_apps)
×
189
    elif path.match("apps.py"):
×
190
        return await _django_app_implicit_dependencies(request, python_setup, django_apps)
×
191
    else:
192
        return InferredDependencies([])
×
193

194

195
def rules():
×
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