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

pantsbuild / pants / 18812500213

26 Oct 2025 03:42AM UTC coverage: 80.284% (+0.005%) from 80.279%
18812500213

Pull #22804

github

web-flow
Merge 2a56fdb46 into 4834308dc
Pull Request #22804: test_shell_command: use correct default cache scope for a test's environment

29 of 31 new or added lines in 2 files covered. (93.55%)

1314 existing lines in 64 files now uncovered.

77900 of 97030 relevant lines covered (80.28%)

3.35 hits per line

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

92.08
/src/python/pants/init/extension_loader.py
1
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
import importlib
12✔
5
import importlib.metadata
12✔
6
import logging
12✔
7
import traceback
12✔
8
from importlib.metadata import Distribution
12✔
9

10
from packaging.requirements import InvalidRequirement, Requirement
12✔
11
from packaging.utils import NormalizedName, canonicalize_name
12✔
12

13
from pants.base.exceptions import BackendConfigurationError
12✔
14
from pants.build_graph.build_configuration import BuildConfiguration
12✔
15
from pants.goal.builtins import register_builtin_goals
12✔
16
from pants.init.import_util import find_matching_distributions
12✔
17
from pants.util.ordered_set import FrozenOrderedSet
12✔
18

19
logger = logging.getLogger(__name__)
12✔
20

21

22
class PluginLoadingError(Exception):
12✔
23
    pass
12✔
24

25

26
class PluginNotFound(PluginLoadingError):
12✔
27
    pass
12✔
28

29

30
class PluginLoadOrderError(PluginLoadingError):
12✔
31
    pass
12✔
32

33

34
def load_backends_and_plugins(
12✔
35
    plugins: list[str],
36
    backends: list[str],
37
    bc_builder: BuildConfiguration.Builder | None = None,
38
) -> BuildConfiguration:
39
    """Load named plugins and source backends.
40

41
    :param plugins: v2 plugins to load.
42
    :param backends: v2 backends to load.
43
    :param bc_builder: The BuildConfiguration (for adding aliases).
44
    """
45
    bc_builder = bc_builder or BuildConfiguration.Builder()
3✔
46
    load_build_configuration_from_source(bc_builder, backends)
3✔
47
    load_plugins(bc_builder, plugins)
3✔
48
    register_builtin_goals(bc_builder)
3✔
49
    return bc_builder.create()
3✔
50

51

52
def load_plugins(
12✔
53
    build_configuration: BuildConfiguration.Builder,
54
    plugins: list[str],
55
) -> None:
56
    """Load named plugins from the current working_set into the supplied build_configuration.
57

58
    "Loading" a plugin here refers to calling registration methods -- it is assumed each plugin
59
    is already on the path and an error will be thrown if it is not. Plugins should define their
60
    entrypoints in the `pantsbuild.plugin` group when configuring their distribution.
61

62
    Like source backends, the `build_file_aliases`, and `register_goals` methods are called if
63
    those entry points are defined.
64

65
    * Plugins are loaded in the order they are provided. *
66

67
    This is important as loading can add, remove or replace existing tasks installed by other plugins.
68

69
    If a plugin needs to assert that another plugin is registered before it, it can define an
70
    entrypoint "load_after" which can return a list of plugins which must have been loaded before it
71
    can be loaded. This does not change the order or what plugins are loaded in any way -- it is
72
    purely an assertion to guard against misconfiguration.
73

74
    :param build_configuration: The BuildConfiguration (for adding aliases).
75
    :param plugins: A list of plugin names optionally with versions, in requirement format.
76
                              eg ['widgetpublish', 'widgetgen==1.2'].
77
    """
78

79
    loaded: dict[NormalizedName, Distribution] = {}
3✔
80
    for plugin in plugins or []:
3✔
UNCOV
81
        try:
1✔
UNCOV
82
            req = Requirement(plugin)
1✔
UNCOV
83
            req_key = canonicalize_name(req.name)
1✔
84
        except InvalidRequirement:
×
85
            raise PluginNotFound(f"Could not find plugin: {req}")
×
86

UNCOV
87
        dists = list(find_matching_distributions(req))
1✔
UNCOV
88
        if not dists:
1✔
UNCOV
89
            raise PluginNotFound(f"Could not find plugin: {req}")
1✔
UNCOV
90
        dist = dists[0]
1✔
91

UNCOV
92
        entry_points = dist.entry_points.select(group="pantsbuild.plugin")
1✔
93

UNCOV
94
        def find_entry_point(entry_point_name: str) -> importlib.metadata.EntryPoint | None:
1✔
UNCOV
95
            for entry_point in entry_points:
1✔
UNCOV
96
                if entry_point.name == entry_point_name:
1✔
UNCOV
97
                    return entry_point
1✔
UNCOV
98
            return None
1✔
99

UNCOV
100
        if load_after_entry_point := find_entry_point("load_after"):
1✔
UNCOV
101
            deps = load_after_entry_point.load()()
1✔
UNCOV
102
            for dep_name in deps:
1✔
UNCOV
103
                dep = Requirement(dep_name)
1✔
UNCOV
104
                dep_key = canonicalize_name(dep.name)
1✔
UNCOV
105
                if dep_key not in loaded:
1✔
UNCOV
106
                    raise PluginLoadOrderError(f"Plugin {plugin} must be loaded after {dep}")
1✔
UNCOV
107
        if target_types_entry_point := find_entry_point("target_types"):
1✔
UNCOV
108
            target_types = target_types_entry_point.load()()
1✔
UNCOV
109
            build_configuration.register_target_types(req_key, target_types)
1✔
UNCOV
110
        if build_file_aliases_entry_point := find_entry_point("build_file_aliases"):
1✔
UNCOV
111
            aliases = build_file_aliases_entry_point.load()()
1✔
UNCOV
112
            build_configuration.register_aliases(aliases)
1✔
UNCOV
113
        if rules_entry_point := find_entry_point("rules"):
1✔
UNCOV
114
            rules = rules_entry_point.load()()
1✔
UNCOV
115
            build_configuration.register_rules(req_key, rules)
1✔
UNCOV
116
        if remote_auth_entry_point := find_entry_point("remote_auth"):
1✔
117
            remote_auth_func = remote_auth_entry_point.load()
×
118
            logger.debug(
×
119
                f"register remote auth function {remote_auth_func.__module__}.{remote_auth_func.__name__} from plugin: {plugin}"
120
            )
121
            build_configuration.register_remote_auth_plugin(remote_auth_func)
×
UNCOV
122
        if auxiliary_goals_entry_point := find_entry_point("auxiliary_goals"):
1✔
123
            auxiliary_goals = auxiliary_goals_entry_point.load()()
×
124
            build_configuration.register_auxiliary_goals(req_key, auxiliary_goals)
×
125

UNCOV
126
        loaded[req_key] = dist
1✔
127

128

129
def load_build_configuration_from_source(
12✔
130
    build_configuration: BuildConfiguration.Builder, backends: list[str]
131
) -> None:
132
    """Installs pants backend packages to provide BUILD file symbols and cli goals.
133

134
    :param build_configuration: The BuildConfiguration (for adding aliases).
135
    :param backends: An list of packages to load v2 backends from.
136
    :raises: :class:``pants.base.exceptions.BuildConfigurationError`` if there is a problem loading
137
      the build configuration.
138
    """
139
    # NB: Backends added here must be explicit dependencies of this module.
140
    backend_packages = FrozenOrderedSet(["pants.core", "pants.backend.project_info", *backends])
4✔
141
    for backend_package in backend_packages:
4✔
142
        load_backend(build_configuration, backend_package)
4✔
143

144

145
def load_backend(build_configuration: BuildConfiguration.Builder, backend_package: str) -> None:
12✔
146
    """Installs the given backend package into the build configuration.
147

148
    :param build_configuration: the BuildConfiguration to install the backend plugin into.
149
    :param backend_package: the package name containing the backend plugin register module that
150
      provides the plugin entrypoints.
151
    :raises: :class:``pants.base.exceptions.BuildConfigurationError`` if there is a problem loading
152
      the build configuration.
153
    """
154
    backend_module = backend_package + ".register"
4✔
155
    try:
4✔
156
        module = importlib.import_module(backend_module)
4✔
UNCOV
157
    except ImportError as ex:
1✔
UNCOV
158
        traceback.print_exc()
1✔
UNCOV
159
        raise BackendConfigurationError(f"Failed to load the {backend_module} backend: {ex!r}")
1✔
160

161
    def invoke_entrypoint(name: str):
4✔
162
        entrypoint = getattr(module, name, lambda: None)
4✔
163
        try:
4✔
164
            return entrypoint()
4✔
UNCOV
165
        except TypeError as e:
1✔
UNCOV
166
            traceback.print_exc()
1✔
UNCOV
167
            raise BackendConfigurationError(
1✔
168
                f"Entrypoint {name} in {backend_module} must be a zero-arg callable: {e!r}"
169
            )
170

171
    target_types = invoke_entrypoint("target_types")
4✔
172
    if target_types:
4✔
173
        build_configuration.register_target_types(backend_package, target_types)
4✔
174
    build_file_aliases = invoke_entrypoint("build_file_aliases")
4✔
175
    if build_file_aliases:
4✔
176
        build_configuration.register_aliases(build_file_aliases)
4✔
177
    rules = invoke_entrypoint("rules")
4✔
178
    if rules:
4✔
179
        build_configuration.register_rules(backend_package, rules)
4✔
180
    remote_auth_func = getattr(module, "remote_auth", None)
4✔
181
    if remote_auth_func:
4✔
182
        logger.debug(
1✔
183
            f"register remote auth function {remote_auth_func.__module__}.{remote_auth_func.__name__} from backend: {backend_package}"
184
        )
185
        build_configuration.register_remote_auth_plugin(remote_auth_func)
1✔
186
    auxiliary_goals = invoke_entrypoint("auxiliary_goals")
4✔
187
    if auxiliary_goals:
4✔
188
        build_configuration.register_auxiliary_goals(backend_package, auxiliary_goals)
×
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