-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cEP-0030: Next Generation Action System
Closes #181
- Loading branch information
1 parent
93c7cce
commit f96a556
Showing
1 changed file
with
355 additions
and
0 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,355 @@ | ||
# Next Generation Action System | ||
|
||
| Metadata | | | ||
| -------- | -----------------------------------------------| | ||
| cEP | 30 | | ||
| Version | 1.0 | | ||
| Title | Next Generation Action System | | ||
| Authors | Akshat Karani <mailto:[email protected]> | | ||
| Status | Proposed | | ||
| Type | Process | | ||
|
||
# Abstract | ||
|
||
This cEP describes the details about Next Generation Action System which | ||
will allow bears to define their own actions as a part of | ||
[GSoC'19 project](https://summerofcode.withgoogle.com/projects/#5450946933424128). | ||
|
||
# Introduction | ||
|
||
Bears run some analysis on a piece of code and output is in form of | ||
a `Result` object. Then some action from a predefined set of actions | ||
is applied to that `Result` object. This system is a bit restrictive | ||
as action from predefined set of actionscan be taken. | ||
If there is a system which will support bears defining their | ||
own actions, then it will make bears more useful. | ||
This project is about changing the current action system so that bears | ||
can define their own actions. | ||
|
||
|
||
# Implementation | ||
|
||
The idea is to add a new attribute to `Result` class which will be a list of | ||
action instances defined by origin of the Result. When bears yield a Result, | ||
they can pass optional argument defining actions. | ||
Then `Result.actions` is added to list of predefined actions when user is asked | ||
for a action to apply. | ||
|
||
## Changing the `Result` class | ||
|
||
1. The first step is to facilitate bears defining their own actions. For this | ||
the `__init__` and `from_values` method of the Result class are be changed. | ||
While yielding Result object a new optional parameter `actions` can be passed. | ||
This is a list of actions that are specific to the bear. | ||
|
||
```python | ||
|
||
class Result: | ||
|
||
# A new parameter `actions` is added | ||
def __init__(self, | ||
origin, | ||
message: str, | ||
affected_code: (tuple, list) = (), | ||
severity: int = RESULT_SEVERITY.NORMAL, | ||
additional_info: str = '', | ||
debug_msg='', | ||
diffs: (dict, None) = None, | ||
confidence: int = 100, | ||
aspect: (aspectbase, None) = None, | ||
message_arguments: dict = {}, | ||
applied_actions: dict = {}, | ||
actions: list = []): | ||
|
||
# A new attribute `actions` is added | ||
self.actions = actions | ||
|
||
# A new parameter `actions` is added | ||
def from_values(cls, | ||
origin, | ||
message: str, | ||
file: str, | ||
line: (int, None) = None, | ||
column: (int, None) = None, | ||
end_line: (int, None) = None, | ||
end_column: (int, None) = None, | ||
severity: int = RESULT_SEVERITY.NORMAL, | ||
additional_info: str = '', | ||
debug_msg='', | ||
diffs: (dict, None) = None, | ||
confidence: int = 100, | ||
aspect: (aspectbase, None) = None, | ||
message_arguments: dict = {}, | ||
actions: list = []): | ||
|
||
# Passing an optional argument `actions` | ||
return cls(origin=origin, | ||
message=message, | ||
affected_code=(source_range,), | ||
severity=severity, | ||
additional_info=additional_info, | ||
debug_msg=debug_msg, | ||
diffs=diffs, | ||
confidence=confidence, | ||
aspect=aspect, | ||
message_arguments=message_arguments, | ||
actions=actions) | ||
|
||
``` | ||
|
||
## Modifying `ConsoleInteraction` module | ||
|
||
1. `ConsoleInteraction` module needs to be modified to add | ||
`Result.actions` to list of predefined actions when asking user | ||
for an action to apply | ||
2. For this `acquire_actions_and_apply` function needs to be modified. | ||
|
||
```python | ||
|
||
def acquire_actions_and_apply(console_printer, | ||
section, | ||
file_diff_dict, | ||
result, | ||
file_dict, | ||
cli_actions=None, | ||
apply_single=False): | ||
cli_actions = CLI_ACTIONS if cli_actions is None else cli_actions | ||
failed_actions = set() | ||
applied_actions = {} | ||
|
||
while True: | ||
action_dict = {} | ||
metadata_list = [] | ||
# Only change is adding result.actions here | ||
for action in list(cli_actions) + result.actions: | ||
# All the applicable actions from cli_actions and result.actions | ||
# are appended to `metadata_list`. | ||
# Then user if asked for action from `metadata_list`. | ||
if action.is_applicable(result, | ||
file_dict, | ||
file_diff_dict, | ||
tuple(applied_actions.keys())) is True: | ||
metadata = action.get_metadata() | ||
action_dict[metadata.name] = action | ||
metadata_list.append(metadata) | ||
|
||
if not metadata_list: | ||
return | ||
|
||
``` | ||
|
||
## Modifying `Processing` module | ||
|
||
1. To allow user to autoapply the actions defined by the bears, | ||
`Processing` module needs to be modified. | ||
2. Definition of `ACTIONS` needs to be modified to contain action | ||
objects instead of action class. | ||
|
||
```python | ||
|
||
ACTIONS = [DoNothingAction(), | ||
ApplyPatchAction(), | ||
PrintDebugMessageAction(), | ||
ShowPatchAction(), | ||
IgnoreResultAction(), | ||
ShowAppliedPatchesAction(), | ||
GeneratePatchesAction()] | ||
|
||
``` | ||
|
||
3. `autoapply_actions` needs to be modified to enable user to | ||
autoapply bear defined actions. | ||
|
||
```python | ||
def autoapply_actions(results, | ||
file_dict, | ||
file_diff_dict, | ||
section, | ||
log_printer=None): | ||
|
||
bear_actions = [] | ||
# bear defined actions from all the results are added to `bear_actions`. | ||
for result in results: | ||
bear_actions += result.actions | ||
|
||
# `bear_actions` is passed as a argument to `get_default_actions` function. | ||
default_actions, invalid_actions = get_default_actions(section, | ||
bear_actions) | ||
no_autoapply_warn = bool(section.get('no_autoapply_warn', False)) | ||
for bearname, actionname in invalid_actions.items(): | ||
logging.warning('Selected default action {!r} for bear {!r} does not ' | ||
'exist. Ignoring action.'.format(actionname, bearname)) | ||
|
||
if len(default_actions) == 0: | ||
return results | ||
|
||
not_processed_results = [] | ||
for result in results: | ||
try: | ||
action = default_actions[result.origin] | ||
except KeyError: | ||
for bear_glob in default_actions: | ||
if fnmatch(result.origin, bear_glob): | ||
action = default_actions[bear_glob] | ||
break | ||
else: | ||
not_processed_results.append(result) | ||
continue | ||
|
||
# This condition checks that if action is in bear_actions which means | ||
# that default action is one defined by a bear, then action must be in | ||
# result.actions because then only that action can be applied to that | ||
# result. | ||
if action not in bear_actions or action in result.actions: | ||
applicable = action.is_applicable(result, | ||
file_dict, | ||
file_diff_dict) | ||
if applicable is not True: | ||
if not no_autoapply_warn: | ||
logging.warning('{}: {}'.format(result.origin, applicable)) | ||
not_processed_results.append(result) | ||
continue | ||
|
||
try: | ||
action.apply_from_section(result, | ||
file_dict, | ||
file_diff_dict, | ||
section) | ||
logging.info('Applied {!r} on {} from {!r}.'.format( | ||
action.get_metadata().name, | ||
result.location_repr(), | ||
result.origin)) | ||
except Exception as ex: | ||
not_processed_results.append(result) | ||
log_exception( | ||
'Failed to execute action {!r} with error: {}.'.format( | ||
action.get_metadata().name, ex), | ||
ex) | ||
logging.debug('-> for result ' + repr(result) + '.') | ||
# Otherwise this result is added to the list of not processed results. | ||
else: | ||
not_processed_results.append(result) | ||
|
||
return not_processed_results | ||
|
||
``` | ||
|
||
4. `get_default_action` function needs to be modified to get default | ||
actions from `bears_actions` also. | ||
|
||
```python | ||
|
||
# A new parameter `bear_actions` is added. | ||
def get_default_actions(section, bear_actions): | ||
try: | ||
default_actions = dict(section['default_actions']) | ||
except IndexError: | ||
return {}, {} | ||
|
||
# `action_dict` now contains all the actions from `ACTIONS` as well as | ||
# bear_actions. | ||
# bears_actions contain action objects, to be consistent with this | ||
# `ACTIONS` was changed to contain action objects. | ||
action_dict = {action.get_metadata().name: action | ||
for action in ACTIONS + bear_actions} | ||
invalid_action_set = default_actions.values() - action_dict.keys() | ||
invalid_actions = {} | ||
if len(invalid_action_set) != 0: | ||
invalid_actions = { | ||
bear: action | ||
for bear, action in default_actions.items() | ||
if action in invalid_action_set} | ||
for invalid in invalid_actions.keys(): | ||
del default_actions[invalid] | ||
|
||
actions = {bearname: action_dict[action_name] | ||
for bearname, action_name in default_actions.items()} | ||
return actions, invalid_actions | ||
|
||
``` | ||
|
||
5. Auto applying actions specific to bears is same as auto-applying | ||
predefined actions. User just need to add | ||
`default_actions = BearName: ActionName` in coafile. | ||
|
||
## Writing bear specific actions | ||
|
||
1. The above changes will now allow bears to define their own actions and | ||
user can apply these actions interactively or by default. | ||
2. While writing any action for bear user needs to implement `is_applicable` | ||
and `apply` method with correct logic. User can also add a `init` method to | ||
pass the necessary data. | ||
3. It may be the case that `is_appicable` method need to be implemented in | ||
such a way that once action is applied is should return False. For this user | ||
can use `file_diff_dict` to know if action has been applied or not. | ||
|
||
### AddNewlineAction for GitCommitBear | ||
|
||
1. `AddNewlineAction` is an action specific to `GitCommitBear`. Whenever | ||
`GitCommitBear` detects that there is no newline between shorlog and | ||
body of the commit message it will yield a `Result` and pass | ||
`AddNewlineAction` as a argument. | ||
|
||
```python | ||
|
||
yield Result(self, | ||
message, | ||
actions=[AddNewlineAction(message, shortlog, body)]) | ||
|
||
``` | ||
|
||
2. Implemtation of `AddNewlineAction` | ||
|
||
```python | ||
|
||
from coalib.misc.Shell import run_shell_command | ||
from coalib.results.result_actions.ResultAction import ResultAction | ||
|
||
|
||
class AddNewlineAction(ResultAction): | ||
|
||
SUCCESS_MESSAGE = 'New Line added successfully.' | ||
|
||
def __init__(self, shortlog, body): | ||
self.shortlog = shortlog | ||
self.body = body | ||
|
||
@staticmethod | ||
def is_applicable(result, | ||
original_file_dict, | ||
file_diff_dict, | ||
applied_actions=()): | ||
return 'AddNewlineAction' not in file_diff_dict | ||
|
||
def apply(self, result, original_file_dict, file_diff_dict, **kwargs): | ||
""" | ||
Add (N)ewline | ||
""" | ||
new_commit_message = '{}\n\n{}'.format(self.shortlog, self.body) | ||
command = 'git commit --amend -m "{}"'.format(new_commit_message) | ||
stdout, err = run_shell_command(command) | ||
# When action is applied a new element is added to | ||
# `file_diff_dict`. This is used in `is_applicable` to know | ||
# if the action was applied or not. | ||
file_diff_dict['AddNewlineAction'] = True | ||
return file_diff_dict | ||
|
||
``` | ||
|
||
3. Some other ideas for actions which can be implemented for GitCommitBear | ||
are: | ||
`OpenEditorAction` - Opens up a editor to edit your commit message. | ||
`FixLinelengthAction` - Fixes that line length of shortlog are body if greater | ||
that specified limit. | ||
|
||
## Bears with Multiple Patches | ||
|
||
1. To support bears with multiple patches a new action `PatchAction` | ||
is implemented. Corresponding to each patch provided for a problem we | ||
have a instance of `PatchAction` in `result.actions`. | ||
2. `PatchAction` is implemented such that it implicitly includes | ||
`ShowPatchAction` and `ApplyPatchAction` for that patch. | ||
3. Initially only `ShowPatchAction` of one patch is applied. | ||
If user wants to apply this patch the `ApplyPatchAction` of that patch | ||
is applied. If the user asks for another patch then `ShowPatchAction` | ||
corresponding to that patch is applied. |