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

pantsbuild / pants / 18517631058

15 Oct 2025 04:18AM UTC coverage: 69.207% (-11.1%) from 80.267%
18517631058

Pull #22745

github

web-flow
Merge 642a76ca1 into 99919310e
Pull Request #22745: [windows] Add windows support in the stdio crate.

53815 of 77759 relevant lines covered (69.21%)

2.42 hits per line

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

90.1
/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
7✔
5
import importlib.metadata
7✔
6
import logging
7✔
7
import traceback
7✔
8
from importlib.metadata import Distribution
7✔
9

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

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

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

21

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

25

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

29

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

33

34
def load_backends_and_plugins(
7✔
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()
2✔
46
    load_build_configuration_from_source(bc_builder, backends)
2✔
47
    load_plugins(bc_builder, plugins)
2✔
48
    register_builtin_goals(bc_builder)
2✔
49
    return bc_builder.create()
2✔
50

51

52
def load_plugins(
7✔
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] = {}
2✔
80
    for plugin in plugins or []:
2✔
81
        try:
1✔
82
            req = Requirement(plugin)
1✔
83
            req_key = canonicalize_name(req.name)
1✔
84
        except InvalidRequirement:
×
85
            raise PluginNotFound(f"Could not find plugin: {req}")
×
86

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

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

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

100
        if load_after_entry_point := find_entry_point("load_after"):
1✔
101
            deps = load_after_entry_point.load()()
1✔
102
            for dep_name in deps:
1✔
103
                dep = Requirement(dep_name)
1✔
104
                dep_key = canonicalize_name(dep.name)
1✔
105
                if dep_key not in loaded:
1✔
106
                    raise PluginLoadOrderError(f"Plugin {plugin} must be loaded after {dep}")
1✔
107
        if target_types_entry_point := find_entry_point("target_types"):
1✔
108
            target_types = target_types_entry_point.load()()
1✔
109
            build_configuration.register_target_types(req_key, target_types)
1✔
110
        if build_file_aliases_entry_point := find_entry_point("build_file_aliases"):
1✔
111
            aliases = build_file_aliases_entry_point.load()()
1✔
112
            build_configuration.register_aliases(aliases)
1✔
113
        if rules_entry_point := find_entry_point("rules"):
1✔
114
            rules = rules_entry_point.load()()
1✔
115
            build_configuration.register_rules(req_key, rules)
1✔
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)
×
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

126
        loaded[req_key] = dist
1✔
127

128

129
def load_build_configuration_from_source(
7✔
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])
2✔
141
    for backend_package in backend_packages:
2✔
142
        load_backend(build_configuration, backend_package)
2✔
143

144

145
def load_backend(build_configuration: BuildConfiguration.Builder, backend_package: str) -> None:
7✔
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"
2✔
155
    try:
2✔
156
        module = importlib.import_module(backend_module)
2✔
157
    except ImportError as ex:
1✔
158
        traceback.print_exc()
1✔
159
        raise BackendConfigurationError(f"Failed to load the {backend_module} backend: {ex!r}")
1✔
160

161
    def invoke_entrypoint(name: str):
2✔
162
        entrypoint = getattr(module, name, lambda: None)
2✔
163
        try:
2✔
164
            return entrypoint()
2✔
165
        except TypeError as e:
1✔
166
            traceback.print_exc()
1✔
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")
2✔
172
    if target_types:
2✔
173
        build_configuration.register_target_types(backend_package, target_types)
2✔
174
    build_file_aliases = invoke_entrypoint("build_file_aliases")
2✔
175
    if build_file_aliases:
2✔
176
        build_configuration.register_aliases(build_file_aliases)
2✔
177
    rules = invoke_entrypoint("rules")
2✔
178
    if rules:
2✔
179
        build_configuration.register_rules(backend_package, rules)
2✔
180
    remote_auth_func = getattr(module, "remote_auth", None)
2✔
181
    if remote_auth_func:
2✔
182
        logger.debug(
×
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)
×
186
    auxiliary_goals = invoke_entrypoint("auxiliary_goals")
2✔
187
    if auxiliary_goals:
2✔
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