diff --git a/src/python/pants/option/alias.py b/src/python/pants/option/alias.py index 7d719f6343b..1e759f8e840 100644 --- a/src/python/pants/option/alias.py +++ b/src/python/pants/option/alias.py @@ -3,35 +3,11 @@ from __future__ import annotations -import logging -import re -import shlex -from dataclasses import dataclass, field -from itertools import chain -from typing import Generator - -from pants.option.errors import OptionsError from pants.option.option_types import DictOption -from pants.option.scope import ScopeInfo from pants.option.subsystem import Subsystem from pants.util.docutil import bin_name -from pants.util.frozendict import FrozenDict from pants.util.strutil import softwrap -logger = logging.getLogger(__name__) - - -class CliAliasError(OptionsError): - pass - - -class CliAliasCycleError(CliAliasError): - pass - - -class CliAliasInvalidError(CliAliasError): - pass - class CliOptions(Subsystem): options_scope = "cli" @@ -57,102 +33,3 @@ class CliOptions(Subsystem): """ ), ) - - -@dataclass(frozen=True) -class CliAlias: - definitions: FrozenDict[str, tuple[str, ...]] = field(default_factory=FrozenDict) - - def __post_init__(self): - valid_alias_re = re.compile(r"(--)?\w(\w|-)*\w$", re.IGNORECASE) - for alias in self.definitions.keys(): - if not re.match(valid_alias_re, alias): - raise CliAliasInvalidError( - softwrap( - f""" - Invalid alias in `[cli].alias` option: {alias!r}. May only contain alpha - numerical letters and the separators `-` and `_`. Flags can be defined using - `--`. A single dash is not allowed. - """ - ) - ) - - @classmethod - def from_dict(cls, aliases: dict[str, str]) -> CliAlias: - definitions = {key: tuple(shlex.split(value)) for key, value in aliases.items()} - - def expand( - definition: tuple[str, ...], *trail: str - ) -> Generator[tuple[str, ...], None, None]: - for arg in definition: - if arg not in definitions: - yield (arg,) - else: - if arg in trail: - raise CliAliasCycleError( - "CLI alias cycle detected in `[cli].alias` option:\n" - + " -> ".join([arg, *trail]) - ) - yield from expand(definitions[arg], arg, *trail) - - return cls( - FrozenDict( - { - alias: tuple(chain.from_iterable(expand(definition))) - for alias, definition in definitions.items() - } - ) - ) - - def check_name_conflicts( - self, known_scopes: dict[str, ScopeInfo], known_flags: dict[str, frozenset[str]] - ) -> None: - for alias in self.definitions.keys(): - scope = known_scopes.get(alias) - - if scope: - raise CliAliasInvalidError( - softwrap( - f""" - Invalid alias in `[cli].alias` option: {alias!r}. This is already a - registered {"goal" if scope.is_goal else "subsystem"}. - """ - ) - ) - - for scope_name, args in known_flags.items(): - for alias in self.definitions.keys(): - if alias in args: - scope_name = scope_name or "global" - raise CliAliasInvalidError( - softwrap( - f""" - Invalid flag-like alias in `[cli].alias` option: {alias!r}. This is - already a registered flag in the {scope_name!r} scope. - """ - ) - ) - - def expand_args(self, args: tuple[str, ...]) -> tuple[str, ...]: - if not self.definitions: - return args - return tuple(self._do_expand_args(args)) - - def _do_expand_args(self, args: tuple[str, ...]) -> Generator[str, None, None]: - args_iter = iter(args) - for arg in args_iter: - if arg == "--": - # Do not expand pass through arguments. - yield arg - yield from args_iter - return - - expanded = self.maybe_expand(arg) - if expanded: - logger.debug(f"Expanded [cli.alias].{arg} => {' '.join(expanded)}") - yield from expanded - else: - yield arg - - def maybe_expand(self, arg: str) -> tuple[str, ...] | None: - return self.definitions.get(arg) diff --git a/src/python/pants/option/alias_test.py b/src/python/pants/option/alias_test.py deleted file mode 100644 index f7e65b5d9a0..00000000000 --- a/src/python/pants/option/alias_test.py +++ /dev/null @@ -1,236 +0,0 @@ -# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). -# Licensed under the Apache License, Version 2.0 (see LICENSE). - -from __future__ import annotations - -from typing import ContextManager - -import pytest - -from pants.option.alias import CliAlias, CliAliasCycleError, CliAliasInvalidError -from pants.option.scope import ScopeInfo -from pants.testutil.pytest_util import no_exception -from pants.util.frozendict import FrozenDict - - -def test_maybe_nothing() -> None: - cli_alias = CliAlias() - assert cli_alias.maybe_expand("arg") is None - - -@pytest.mark.parametrize( - "alias, expanded", - [ - ("--arg1", ("--arg1",)), - ("--arg1 --arg2", ("--arg1", "--arg2")), - ("--arg=value --option", ("--arg=value", "--option")), - ("--arg=value --option flag", ("--arg=value", "--option", "flag")), - ("--arg 'quoted value'", ("--arg", "quoted value")), - ], -) -def test_maybe_expand_alias(alias: str, expanded: tuple[str, ...] | None) -> None: - cli_alias = CliAlias.from_dict( - { - "alias": alias, - } - ) - assert cli_alias.maybe_expand("alias") == expanded - - cli_alias = CliAlias.from_dict( - { - "--alias": alias, - } - ) - assert cli_alias.maybe_expand("--alias") == expanded - - -@pytest.mark.parametrize( - "args, expanded", - [ - ( - ("some", "alias", "target"), - ("some", "--flag", "goal", "target"), - ), - ( - # Don't touch pass through args. - ("some", "--", "alias", "target"), - ("some", "--", "alias", "target"), - ), - ], -) -def test_expand_args(args: tuple[str, ...], expanded: tuple[str, ...]) -> None: - cli_alias = CliAlias.from_dict( - { - "alias": "--flag goal", - } - ) - assert cli_alias.expand_args(args) == expanded - - -@pytest.mark.parametrize( - "args, expanded", - [ - ( - ("some", "--alias", "target"), - ("some", "--flag", "goal", "target"), - ), - ( - # Don't touch pass through args. - ("some", "--", "--alias", "target"), - ("some", "--", "--alias", "target"), - ), - ], -) -def test_expand_args_flag(args: tuple[str, ...], expanded: tuple[str, ...]) -> None: - cli_alias = CliAlias.from_dict( - { - "--alias": "--flag goal", - } - ) - assert cli_alias.expand_args(args) == expanded - - -def test_no_expand_when_no_aliases() -> None: - args = ("./pants",) - cli_alias = CliAlias() - assert cli_alias.expand_args(args) is args - - -@pytest.mark.parametrize( - "alias, definitions", - [ - ( - { - "basic": "goal", - "nested": "--option=advanced basic", - }, - { - "basic": ("goal",), - "nested": ( - "--option=advanced", - "goal", - ), - }, - ), - ( - { - "multi-nested": "deep nested", - "basic": "goal", - "nested": "--option=advanced basic", - }, - { - "multi-nested": ("deep", "--option=advanced", "goal"), - "basic": ("goal",), - "nested": ( - "--option=advanced", - "goal", - ), - }, - ), - ( - { - "cycle": "other-alias", - "other-alias": "cycle", - }, - pytest.raises( - CliAliasCycleError, - match=( - r"CLI alias cycle detected in `\[cli\]\.alias` option:\n" - + r"other-alias -> cycle -> other-alias" - ), - ), - ), - ( - { - "cycle": "--other-alias", - "--other-alias": "cycle", - }, - pytest.raises( - CliAliasCycleError, - match=( - r"CLI alias cycle detected in `\[cli\]\.alias` option:\n" - + r"--other-alias -> cycle -> --other-alias" - ), - ), - ), - ( - { - "--cycle": "--other-alias", - "--other-alias": "--cycle", - }, - pytest.raises( - CliAliasCycleError, - match=( - r"CLI alias cycle detected in `\[cli\]\.alias` option:\n" - + r"--other-alias -> --cycle -> --other-alias" - ), - ), - ), - ], -) -def test_nested_alias(alias, definitions: dict | ContextManager) -> None: - expect: ContextManager = no_exception() if isinstance(definitions, dict) else definitions - with expect: - cli_alias = CliAlias.from_dict(alias) - if isinstance(definitions, dict): - assert cli_alias.definitions == FrozenDict(definitions) - - -@pytest.mark.parametrize( - "alias", - [ - # Check that we do not allow any alias that may resemble a valid option/spec. - "dir/spec", - "file.name", - "target:name", - "-o", - "-option", - ], -) -def test_invalid_alias_name(alias: str) -> None: - with pytest.raises( - CliAliasInvalidError, match=(f"Invalid alias in `\\[cli\\]\\.alias` option: {alias!r}\\.") - ): - CliAlias.from_dict({alias: ""}) - - -def test_banned_alias_names() -> None: - cli_alias = CliAlias.from_dict({"fmt": "--cleverness format"}) - with pytest.raises( - CliAliasInvalidError, - match=( - r"Invalid alias in `\[cli\]\.alias` option: 'fmt'\. This is already a registered goal\." - ), - ): - cli_alias.check_name_conflicts({"fmt": ScopeInfo("fmt", is_goal=True)}, {}) - - -@pytest.mark.parametrize( - "alias, info, expected", - [ - ( - {"--keep-sandboxes": "--foobar"}, - {"": "--keep-sandboxes"}, - pytest.raises( - CliAliasInvalidError, - match=( - r"Invalid flag-like alias in `\[cli\]\.alias` option: '--keep-sandboxes'\. This is already a registered flag in the 'global' scope\." - ), - ), - ), - ( - {"--changed-since": "--foobar"}, - {"changed": "--changed-since"}, - pytest.raises( - CliAliasInvalidError, - match=( - r"Invalid flag-like alias in `\[cli\]\.alias` option: '--changed-since'\. This is already a registered flag in the 'changed' scope\." - ), - ), - ), - ], -) -def test_banned_alias_flag_names(alias, info, expected) -> None: - cli_alias = CliAlias.from_dict(alias) - with expected: - cli_alias.check_name_conflicts({}, info)