Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code Quality Tool: toml-based backend templating #20270

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pants.backend.adhoc.code_quality_tool import CodeQualityToolRuleBuilder


def rules(backend_package_alias: str, goal: str, target: str, name: str):
cfg = CodeQualityToolRuleBuilder(
goal=goal,
target=target,
name=name,
scope=backend_package_alias,
)
return cfg.rules()
71 changes: 52 additions & 19 deletions src/python/pants/init/extension_loader.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from dataclasses import dataclass

import importlib
import logging
import traceback
from typing import Dict, List, Optional

from pants.util.frozendict import FrozenDict
from typing import Dict, List, Optional, Mapping, Any, cast

from pkg_resources import Requirement, WorkingSet

Expand All @@ -28,12 +31,23 @@ class PluginLoadOrderError(PluginLoadingError):
pass


def load_backends_and_plugins(
plugins: List[str],
working_set: WorkingSet,
backends: List[str],
bc_builder: Optional[BuildConfiguration.Builder] = None,
) -> BuildConfiguration:
@dataclass(frozen=True)
class TemplatedBackendConfig:
template: str
kwargs: FrozenDict[str, Any]

@classmethod
def from_dict(cls, d: Any):
d = dict(d)
template = d.pop('template', None)
if not template:
raise ValueError(f'"template" is a required key for a backend template')
return cls(template=cast(str, template), kwargs=FrozenDict(d))
gauthamnair marked this conversation as resolved.
Show resolved Hide resolved


def load_backends_and_plugins(plugins: List[str], working_set: WorkingSet, backends: List[str],
bc_builder: Optional[BuildConfiguration.Builder] = None,
templated_backends: Optional[Mapping[str, TemplatedBackendConfig]] = None) -> BuildConfiguration:
"""Load named plugins and source backends.

:param plugins: v2 plugins to load.
Expand All @@ -42,7 +56,7 @@ def load_backends_and_plugins(
:param bc_builder: The BuildConfiguration (for adding aliases).
"""
bc_builder = bc_builder or BuildConfiguration.Builder()
load_build_configuration_from_source(bc_builder, backends)
load_build_configuration_from_source(bc_builder, backends, templated_backends=templated_backends)
load_plugins(bc_builder, plugins, working_set)
register_builtin_goals(bc_builder)
return bc_builder.create()
Expand Down Expand Up @@ -111,9 +125,8 @@ def load_plugins(
loaded[dist.as_requirement().key] = dist


def load_build_configuration_from_source(
build_configuration: BuildConfiguration.Builder, backends: List[str]
) -> None:
def load_build_configuration_from_source(build_configuration: BuildConfiguration.Builder, backends: List[str],
templated_backends: Optional[Mapping[str, TemplatedBackendConfig]] = None) -> None:
"""Installs pants backend packages to provide BUILD file symbols and cli goals.

:param build_configuration: The BuildConfiguration (for adding aliases).
Expand All @@ -123,11 +136,15 @@ def load_build_configuration_from_source(
"""
# NB: Backends added here must be explicit dependencies of this module.
backend_packages = FrozenOrderedSet(["pants.core", "pants.backend.project_info", *backends])
templated_backends = templated_backends or {}

for backend_package in backend_packages:
load_backend(build_configuration, backend_package)
load_backend(build_configuration, backend_package,
templating_config=templated_backends.get(backend_package))


def load_backend(build_configuration: BuildConfiguration.Builder, backend_package: str) -> None:
def load_backend(build_configuration: BuildConfiguration.Builder, backend_package: str,
templating_config: Optional[TemplatedBackendConfig]) -> None:
"""Installs the given backend package into the build configuration.

:param build_configuration: the BuildConfiguration to install the backend plugin into.
Expand All @@ -136,22 +153,38 @@ def load_backend(build_configuration: BuildConfiguration.Builder, backend_packag
:raises: :class:``pants.base.exceptions.BuildConfigurationError`` if there is a problem loading
the build configuration.
"""
backend_module = backend_package + ".register"

if templating_config:
kwargs = {'backend_package_alias': backend_package}
kwargs.update(templating_config.kwargs)
backend_module = templating_config.template + ".register"
else:
kwargs = {}
backend_module = backend_package + ".register"

try:
module = importlib.import_module(backend_module)
except ImportError as ex:
traceback.print_exc()
raise BackendConfigurationError(f"Failed to load the {backend_module} backend: {ex!r}")

def return_none(**kwargs):
return None

def invoke_entrypoint(name: str):
entrypoint = getattr(module, name, lambda: None)
entrypoint = getattr(module, name, return_none)
try:
return entrypoint()
return entrypoint(**kwargs)
except TypeError as e:
traceback.print_exc()
raise BackendConfigurationError(
f"Entrypoint {name} in {backend_module} must be a zero-arg callable: {e!r}"
)
if not kwargs:
err_msg = f"Entrypoint {name} in {backend_module} must be a zero-arg callable: {e!r}"
else:
err_msg = (
f"Entrypoint {name} in {backend_module} backend template "
f"must accept {list(kwargs)} as keyword arguments: {e!r}"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be improved by using inspect.signature(entrypoint) to also present the actual keyword args expected.

Would probably also be helpful to include kwargs["backend_package_alias"] in the error message, to help identify which templating section this was for, as the backend module could be in multiple of them.

raise BackendConfigurationError(err_msg)

target_types = invoke_entrypoint("target_types")
if target_types:
Expand Down
8 changes: 7 additions & 1 deletion src/python/pants/init/options_initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from pants.init.engine_initializer import EngineInitializer
from pants.init.extension_loader import (
load_backends_and_plugins,
load_build_configuration_from_source,
load_build_configuration_from_source, TemplatedBackendConfig,
)
from pants.init.plugin_resolver import PluginResolver
from pants.init.plugin_resolver import rules as plugin_resolver_rules
Expand Down Expand Up @@ -57,11 +57,17 @@ def _initialize_build_configuration(
sys.path.append(path)
pkg_resources.fixup_namespace_packages(path)

templated_backends = {
backend_alias: TemplatedBackendConfig.from_dict(templating_config)
for backend_alias, templating_config in bootstrap_options.templated_backends.items()
}

# Load plugins and backends.
return load_backends_and_plugins(
bootstrap_options.plugins,
working_set,
bootstrap_options.backend_packages,
templated_backends=templated_backends,
)


Expand Down
5 changes: 5 additions & 0 deletions src/python/pants/option/global_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ class BootstrapOptions:
"""
),
)
templated_backends = DictOption(
advanced=True,
default={},
help="TODO",
)
plugins = StrListOption(
advanced=True,
help=softwrap(
Expand Down
5 changes: 2 additions & 3 deletions tests/python/pants_test/init/test_extension_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,8 @@ def reg_alias():
aliases = BuildFileAliases(objects={"override-alias": DummyObject1})
with self.create_register(build_file_aliases=lambda: aliases) as backend_module:
backends = [backend_module]
build_configuration = load_backends_and_plugins(
plugins, self.working_set, backends, bc_builder=self.bc_builder
)
build_configuration = load_backends_and_plugins(plugins, self.working_set, backends,
bc_builder=self.bc_builder)
# The backend should load first, then the plugins, therefore the alias registered in
# the plugin will override the alias registered by the backend
registered_aliases = build_configuration.registered_aliases
Expand Down
Loading