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

[internal] Check for ambiguity when running generate-lockfiles #14178

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 71 additions & 12 deletions src/python/pants/core/goals/generate_lockfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

import itertools
import logging
from collections import defaultdict
from dataclasses import dataclass
from enum import Enum
from typing import ClassVar, Iterable, Sequence, cast

from pants.engine.collection import Collection
Expand Down Expand Up @@ -131,22 +133,77 @@ def __init__(
)


class _ResolveProviderType(Enum):
TOOL = 1
USER = 2


@dataclass(frozen=True, order=True)
class _ResolveProvider:
option_name: str
type_: _ResolveProviderType


class AmbiguousResolveNamesError(Exception):
def __init__(self, ambiguous_names: list[str]) -> None:
if len(ambiguous_names) == 1:
first_paragraph = (
"A resolve name from the option `[python].experimental_resolves` collides with the "
f"name of a tool resolve: {ambiguous_names[0]}"
)
def __init__(self, ambiguous_name: str, providers: set[_ResolveProvider]) -> None:
tool_providers = []
user_providers = []
for provider in sorted(providers):
if provider.type_ == _ResolveProviderType.TOOL:
tool_providers.append(provider.option_name)
else:
user_providers.append(provider.option_name)

if tool_providers:
if not user_providers:
raise AssertionError(
f"{len(tool_providers)} tools have the same options_scope: {ambiguous_name}. "
"If you're writing a plugin, rename your `ToolLockfileSentinel`s so that "
"there is no ambiguity. Otherwise, please open a bug at "
"https://github.com/pantsbuild/pants/issues/new."
)
if len(user_providers) == 1:
msg = (
f"A resolve name from the option `{user_providers[0]}` collides with the "
f"name of a tool resolve: {ambiguous_name}\n\n"
f"To fix, please update `{user_providers[0]}` to use a different resolve name."
)
else:
msg = (
f"Multiple options define the resolve name `{ambiguous_name}`, but it is "
f"already claimed by a tool: {user_providers}\n\n"
f"To fix, please update these options so that none of them use "
f"`{ambiguous_name}`."
)
else:
first_paragraph = (
"Some resolve names from the option `[python].experimental_resolves` collide with "
f"the names of tool resolves: {sorted(ambiguous_names)}"
assert len(user_providers) > 1
msg = (
f"The same resolve name `{ambiguous_name}` is used by multiple options, which "
f"causes ambiguity: {user_providers}\n\n"
f"To fix, please update these options so that `{ambiguous_name}` is not used more "
f"than once."
)
super().__init__(
f"{first_paragraph}\n\n"
"To fix, please update `[python].experimental_resolves` to use different resolve names."
super().__init__(msg)


def _check_ambiguous_resolve_names(
all_known_user_resolve_names: Iterable[KnownUserResolveNames],
all_tool_sentinels: Iterable[type[ToolLockfileSentinel]],
) -> None:
resolve_name_to_providers = defaultdict(set)
for sentinel in all_tool_sentinels:
resolve_name_to_providers[sentinel.options_scope].add(
_ResolveProvider(sentinel.options_scope, _ResolveProviderType.TOOL)
)
for known_user_resolve_names in all_known_user_resolve_names:
for resolve_name in known_user_resolve_names.names:
resolve_name_to_providers[resolve_name].add(
_ResolveProvider(known_user_resolve_names.option_name, _ResolveProviderType.USER)
)

for resolve_name, providers in resolve_name_to_providers.items():
if len(providers) > 1:
raise AmbiguousResolveNamesError(resolve_name, providers)


def determine_resolves_to_generate(
Expand All @@ -158,6 +215,8 @@ def determine_resolves_to_generate(

Return a tuple of `(user_resolves, tool_lockfile_sentinels)`.
"""
_check_ambiguous_resolve_names(all_known_user_resolve_names, all_tool_sentinels)

resolve_names_to_sentinels = {
sentinel.options_scope: sentinel for sentinel in all_tool_sentinels
}
Expand Down
41 changes: 33 additions & 8 deletions src/python/pants/core/goals/generate_lockfiles_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pants.core.goals.generate_lockfiles import (
DEFAULT_TOOL_LOCKFILE,
NO_TOOL_LOCKFILE,
AmbiguousResolveNamesError,
KnownUserResolveNames,
LockfileRequest,
RequestedUserResolveNames,
Expand Down Expand Up @@ -74,15 +75,39 @@ def assert_chosen(
with pytest.raises(UnrecognizedResolveNamesError):
assert_chosen({"fake"}, expected_user_resolves=[], expected_tools=[])

# TODO: Add ambiguity checks.
# Error if same resolve name used for tool lockfiles and user lockfiles.
# class AmbiguousTool(ToolLockfileSentinel):
# options_scope = "ambiguous"
#
# with pytest.raises(AmbiguousResolveNamesError):
# determine_resolves_to_generate(
# {"ambiguous": "lockfile.txt"}, [AmbiguousTool], ["ambiguous"]
# )
class AmbiguousTool(ToolLockfileSentinel):
options_scope = "ambiguous"

with pytest.raises(AmbiguousResolveNamesError):
determine_resolves_to_generate(
[
KnownUserResolveNames(
("ambiguous",),
"[lang].resolves",
requested_resolve_names_cls=Lang1Requested,
)
],
[AmbiguousTool],
set(),
)
with pytest.raises(AmbiguousResolveNamesError):
determine_resolves_to_generate(
[
KnownUserResolveNames(
("ambiguous",),
"[lang1].resolves",
requested_resolve_names_cls=Lang1Requested,
),
KnownUserResolveNames(
("ambiguous",),
"[lang2].resolves",
requested_resolve_names_cls=Lang1Requested,
),
],
[],
set(),
)


def test_filter_tool_lockfile_requests() -> None:
Expand Down