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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 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/detect_apps.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
UNCOV
3
from __future__ import annotations
×
4

UNCOV
5
import json
×
UNCOV
6
import os
×
UNCOV
7
from collections import defaultdict
×
UNCOV
8
from collections.abc import Iterable
×
UNCOV
9
from dataclasses import dataclass
×
10

UNCOV
11
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
12
from pants.backend.python.target_types import InterpreterConstraintsField, PythonResolveField
×
UNCOV
13
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
UNCOV
14
from pants.backend.python.util_rules.pex import find_interpreter
×
UNCOV
15
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
×
UNCOV
16
from pants.base.specs import FileGlobSpec, RawSpecs
×
UNCOV
17
from pants.engine.fs import AddPrefix, CreateDigest, FileContent, MergeDigests
×
UNCOV
18
from pants.engine.internals.graph import hydrate_sources, resolve_targets
×
UNCOV
19
from pants.engine.internals.selectors import concurrently
×
UNCOV
20
from pants.engine.intrinsics import add_prefix, create_digest, merge_digests
×
UNCOV
21
from pants.engine.process import Process, execute_process_or_raise
×
UNCOV
22
from pants.engine.rules import Rule, collect_rules, implicitly, rule
×
UNCOV
23
from pants.engine.target import HydrateSourcesRequest, SourcesField, Target
×
UNCOV
24
from pants.util.frozendict import FrozenDict
×
UNCOV
25
from pants.util.resources import read_resource
×
26

27

UNCOV
28
@dataclass(frozen=True)
×
UNCOV
29
class DjangoApp:
×
UNCOV
30
    name: str
×
UNCOV
31
    config_file: str
×
32

33

UNCOV
34
class DjangoApps(FrozenDict[str, DjangoApp]):
×
UNCOV
35
    @property
×
UNCOV
36
    def label_to_name(self) -> FrozenDict[str, str]:
×
37
        return FrozenDict((label, app.name) for label, app in self.items())
×
38

UNCOV
39
    @property
×
UNCOV
40
    def label_to_file(self) -> FrozenDict[str, str]:
×
41
        return FrozenDict((label, app.config_file) for label, app in self.items())
×
42

UNCOV
43
    def add_from_json(self, json_bytes: bytes, strip_prefix: str = "") -> DjangoApps:
×
44
        json_dict: dict[str, dict[str, str]] = json.loads(json_bytes.decode())
×
45
        apps = {
×
46
            label: DjangoApp(
47
                val["app_name"], val["config_file"].partition(f"{strip_prefix}{os.sep}")[2]
48
            )
49
            for label, val in json_dict.items()
50
        }
51
        combined = dict(self, **apps)
×
52
        return DjangoApps(sorted(combined.items()))
×
53

54

UNCOV
55
_script_resource = "scripts/app_detector.py"
×
56

57

UNCOV
58
@rule
×
UNCOV
59
async def detect_django_apps(python_setup: PythonSetup) -> DjangoApps:
×
60
    # A Django app has a "name" - the full import path to the app ("path.to.myapp"),
61
    # and a "label" - a short name, usually the last segment of the import path ("myapp").
62
    #
63
    # An app provides this information via a subclass of AppConfig, living in a
64
    # file named apps.py.  Django loads this information into an app registry at runtime.
65
    #
66
    # Some parts of Django, notably migrations, use the label to reference apps. So to do custom
67
    # Django dep inference, we need to know the label -> name mapping.
68
    #
69
    # The only truly correct way to enumerate Django apps is to run the Django app registry code.
70
    # However we can't do this until after dep inference has completed, and even then it would be
71
    # complicated: we wouldn't know which settings.py to use, or whether it's safe to run Django
72
    # against that settings.py. Instead, we do this statically via parsing the apps.py file.
73
    #
74
    # NB: Legacy Django apps may not have an apps.py, in which case the label is assumed to be
75
    #  the name of the app dir, but the recommendation for many years has been to have it, and
76
    #  the Django startapp tool creates it for you. If an app does not have such an apps.py,
77
    #  then we won't be able to infer deps on that app unless we find other ways of detecting it.
78
    #  We should only do that if that case turns out to be common, and for some reason users can't
79
    #  simply create an apps.py to fix the issue.
80
    #
81
    # NB: Right now we only detect first-party apps in repo. We assume that third-party apps will
82
    #  be dep-inferred as a whole via the full package path in settings.py anyway.
83
    #  In the future we may find a way to map third-party apps here as well.
84
    django_apps = DjangoApps(FrozenDict())
×
85
    targets = await resolve_targets(
×
86
        **implicitly(
87
            RawSpecs.create(
88
                specs=[FileGlobSpec("**/apps.py")],
89
                description_of_origin="Django app detection",
90
                unmatched_glob_behavior=GlobMatchErrorBehavior.ignore,
91
            )
92
        )
93
    )
94
    if not targets:
×
95
        return django_apps
×
96

97
    script_file_content = FileContent(
×
98
        "script/__visitor.py", read_resource(__name__, _script_resource)
99
    )
100
    script_digest = await create_digest(CreateDigest([script_file_content]))
×
101
    apps_sandbox_prefix = "_apps_to_detect"
×
102

103
    # Partition by ICs, so we can run the detector on the appropriate interpreter.
104
    ics_to_tgts: dict[InterpreterConstraints, list[Target]] = defaultdict(list)
×
105
    for tgt in targets:
×
106
        ics = InterpreterConstraints(
×
107
            tgt[InterpreterConstraintsField].value_or_configured_default(
108
                python_setup, tgt[PythonResolveField] if tgt.has_field(PythonResolveField) else None
109
            )
110
        )
111
        ics_to_tgts[ics].append(tgt)
×
112

113
    for ics, tgts in ics_to_tgts.items():
×
114
        sources = await concurrently(  # noqa: PNT30: requires triage
×
115
            [
116
                hydrate_sources(HydrateSourcesRequest(tgt[SourcesField]), **implicitly())
117
                for tgt in tgts
118
            ]
119
        )
120
        apps_digest = await merge_digests(MergeDigests([src.snapshot.digest for src in sources]))
×
121
        prefixed_apps_digest = await add_prefix(AddPrefix(apps_digest, apps_sandbox_prefix))
×
122

123
        input_digest = await merge_digests(MergeDigests([prefixed_apps_digest, script_digest]))
×
124
        python_interpreter = await find_interpreter(ics, **implicitly())
×
125

126
        process_result = await execute_process_or_raise(
×
127
            **implicitly(
128
                Process(
129
                    argv=[
130
                        python_interpreter.path,
131
                        script_file_content.path,
132
                        apps_sandbox_prefix,
133
                    ],
134
                    input_digest=input_digest,
135
                    description="Detect Django apps",
136
                )
137
            )
138
        )
139
        django_apps = django_apps.add_from_json(
×
140
            process_result.stdout or b"{}", strip_prefix=apps_sandbox_prefix
141
        )
142

143
    return django_apps
×
144

145

UNCOV
146
def rules() -> Iterable[Rule]:
×
UNCOV
147
    return collect_rules()
×
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