-
-
Notifications
You must be signed in to change notification settings - Fork 636
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add a --query option to unify target selection
use the ast module to parse query expressions!! # Rust tests will be skipped. Delete if not intended. [ci skip-rust-tests]
- Loading branch information
1 parent
22cc05f
commit 0cec7af
Showing
6 changed files
with
333 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
import ast | ||
from abc import ABC, abstractmethod | ||
from dataclasses import dataclass | ||
from typing import Any, Dict, Tuple, Type, TypeVar | ||
|
||
from pants.build_graph.address import Address | ||
from pants.engine.addresses import Addresses | ||
from pants.engine.internals.graph import Owners, OwnersRequest | ||
from pants.engine.rules import RootRule, rule | ||
from pants.engine.selectors import Get | ||
from pants.engine.unions import UnionMembership, UnionRule, union | ||
from pants.scm.subsystems.changed import ChangedOptions, ChangedAddresses, ChangedRequest, DependeesOption, UncachedScmWrapper | ||
from pants.util.meta import classproperty | ||
from pants.util.strutil import safe_shlex_split | ||
|
||
|
||
@union | ||
class QueryComponent(ABC): | ||
|
||
@classproperty | ||
@abstractmethod | ||
def function_name(cls): | ||
"""The initial argument of a shlexed query expression. | ||
If the user provides --query='<name> <args...>' on the command line, and `<name>` matches this | ||
property, the .parse_from_args() method is invoked with `<args...>` (shlexed, so split by | ||
spaces). | ||
""" | ||
|
||
@classmethod | ||
@abstractmethod | ||
def parse_from_args(cls, *args): | ||
"""Create an instance of this class from variadic positional string arguments. | ||
This method should raise an error if the args are incorrect or invalid. | ||
""" | ||
|
||
|
||
@dataclass(frozen=True) | ||
class QueryAddresses: | ||
addresses: Addresses | ||
|
||
|
||
@dataclass(frozen=True) | ||
class OwnerOf(QueryComponent): | ||
files: Tuple[str] | ||
|
||
function_name = 'owner_of' | ||
|
||
@classmethod | ||
def parse_from_args(cls, *args): | ||
return cls(files=tuple([str(f) for f in args])) | ||
|
||
|
||
@rule | ||
async def owner_of_request(owner_of: OwnerOf) -> QueryAddresses: | ||
request = OwnersRequest(sources=owner_of.files) | ||
owners = await Get(Owners, OwnersRequest, request) | ||
return QueryAddresses(Addresses(bfa.to_address() for bfa in owners.addresses)) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ChangesSince(QueryComponent): | ||
since: str | ||
dependees: DependeesOption | ||
|
||
function_name = 'since' | ||
|
||
@classmethod | ||
def parse_from_args(cls, since, dependees=DependeesOption.NONE): | ||
return cls(since=str(since), | ||
dependees=DependeesOption(dependees)) | ||
|
||
|
||
@rule | ||
async def since_request( | ||
scm_wrapper: UncachedScmWrapper, | ||
since: ChangesSince, | ||
) -> QueryAddresses: | ||
scm = scm_wrapper.scm | ||
changed_options = ChangedOptions( | ||
since=since.since, | ||
diffspec=None, | ||
dependees=since.dependees, | ||
) | ||
changed = await Get(ChangedAddresses, ChangedRequest( | ||
sources=tuple(changed_options.changed_files(scm=scm)), | ||
dependees=changed_options.dependees, | ||
)) | ||
return QueryAddresses(changed.addresses) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ChangesForDiffspec(QueryComponent): | ||
diffspec: str | ||
dependees: DependeesOption | ||
|
||
function_name = 'changes_for_diffspec' | ||
|
||
@classmethod | ||
def parse_from_args(cls, diffspec, dependees=DependeesOption.NONE): | ||
return cls(diffspec=str(diffspec), | ||
dependees=DependeesOption(dependees)) | ||
|
||
|
||
@rule | ||
async def changes_for_diffspec_request( | ||
scm_wrapper: UncachedScmWrapper, | ||
changes_for_diffspec: ChangesForDiffspec, | ||
) -> QueryAddresses: | ||
scm = scm_wrapper.scm | ||
changed_options = ChangedOptions( | ||
since=None, | ||
diffspec=changes_for_diffspec.diffspec, | ||
dependees=changes_for_diffspec.dependees, | ||
) | ||
changed = await Get(ChangedAddresses, ChangedRequest( | ||
sources=tuple(changed_options.changed_files(scm=scm)), | ||
dependees=changed_options.dependees, | ||
)) | ||
return QueryAddresses(changed.addresses) | ||
|
||
|
||
_T = TypeVar('_T', bound=QueryComponent) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class KnownQueryExpressions: | ||
components: Dict[str, Type[_T]] | ||
|
||
|
||
@rule | ||
def known_query_expressions(union_membership: UnionMembership) -> KnownQueryExpressions: | ||
return KnownQueryExpressions({ | ||
union_member.function_name: union_member | ||
for union_member in union_membership[QueryComponent] | ||
}) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class QueryParseInput: | ||
expr: str | ||
|
||
|
||
class QueryParseError(Exception): pass | ||
|
||
|
||
@dataclass(frozen=True) | ||
class QueryComponentWrapper: | ||
underlying: _T | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ParsedPythonesqueFunctionCall: | ||
"""Representation of a limited form of python named function calls.""" | ||
function_name: str | ||
positional_args: Tuple[Any, ...] | ||
keyword_args: Dict[str, Any] | ||
|
||
|
||
def _parse_python_arg(arg_value: ast.AST) -> Any: | ||
"""Convert an AST node for the argument of a function call into its literal value.""" | ||
return ast.literal_eval(arg_value) | ||
|
||
|
||
def _parse_python_esque_function_call(expr: str) -> ParsedPythonesqueFunctionCall: | ||
"""Parse a string into a description of a python function call expression.""" | ||
try: | ||
query_expression = ast.parse(expr).body[0].value | ||
except Exception as e: | ||
raise QueryParseError(f'Error parsing query expression: {e}') from e | ||
|
||
if not isinstance(query_expression, ast.Call): | ||
type_name = type(query_expression).__name__ | ||
raise QueryParseError( | ||
f'Query expression must be a single function call, but received {type_name}: ' | ||
f'{ast.dump(query_expression)}.') | ||
|
||
func_expr = query_expression.func | ||
if not isinstance(func_expr, ast.Name): | ||
raise QueryParseError('Function call in query expression should just be a name, but ' | ||
f'received {type(func_expr).__name__}: {ast.dump(func_expr)}.') | ||
function_name = func_expr.id | ||
|
||
positional_args = [_parse_python_arg(x) for x in query_expression.args] | ||
keyword_args = { | ||
k.arg: _parse_python_arg(k.value) | ||
for k in query_expression.keywords | ||
} | ||
|
||
return ParsedPythonesqueFunctionCall( | ||
function_name=function_name, | ||
positional_args=positional_args, | ||
keyword_args=keyword_args, | ||
) | ||
|
||
|
||
# FIXME: allow returning an @union!!! | ||
@rule | ||
def parse_query_expr(s: QueryParseInput, known: KnownQueryExpressions) -> QueryComponentWrapper: | ||
"""Parse the input string and attempt to find a query function matching the function call. | ||
:return: A query component which can be resolved into `BuildFileAddresses` in the v2 engine. | ||
""" | ||
try: | ||
parsed_function_call = _parse_python_esque_function_call(s.expr) | ||
except Exception as e: | ||
raise QueryParseError(f'Error parsing expression {s}: {e}.') from e | ||
|
||
name = parsed_function_call.function_name | ||
args = parsed_function_call.positional_args | ||
kwargs = parsed_function_call.keyword_args | ||
|
||
selected_function = known.components.get(name, None) | ||
if selected_function: | ||
return QueryComponentWrapper(selected_function.parse_from_args(*args, **kwargs)) | ||
else: | ||
raise QueryParseError( | ||
f'Query function with name {name} not found (in expr {s})! The known functions are: {known}.') | ||
|
||
|
||
def rules(): | ||
return [ | ||
RootRule(OwnerOf), | ||
RootRule(ChangesSince), | ||
RootRule(QueryParseInput), | ||
RootRule(ChangesForDiffspec), | ||
known_query_expressions, | ||
UnionRule(QueryComponent, OwnerOf), | ||
UnionRule(QueryComponent, ChangesSince), | ||
UnionRule(QueryComponent, ChangesForDiffspec), | ||
owner_of_request, | ||
since_request, | ||
changes_for_diffspec_request, | ||
parse_query_expr, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.