| Metadata | |
|---|---|
| cEP | 30 |
| Version | 1.0 |
| Title | Next Generation Action System |
| Authors | Akshat Karani mailto:akshatkarani@gmail.com |
| Status | Proposed |
| Type | Process |
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.
Bears run some analysis on a piece of code and output is in the 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 only an action from predefined set of actions can 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 modifying the current action system so that bears
can define their own actions.
This project also aims to implement a new action which will help in
supporting for bears to provide multiple patches for the affected code.
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 actions in result.actions are added to the list of predefined actions
when the user is asked for an action to apply.
- The first step is to facilitate bears defining their own actions. For this
the
__init__andfrom_valuesmethod of the Result class are to be changed. While yielding Result object a new optional parameteractionscan be passed. This is a list of actions that are specific to the bear.
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)ConsoleInteractionmodule needs to be modified to add the actions inresult.actionsto the list of predefined actions when asking user for an action to apply- For this
acquire_actions_and_applyfunction needs to be modified.
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 an 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- To allow user to autoapply the actions defined by the bears, some changes
are to be made in the
Processingmodule. autoapply_actionsneeds to be modified.result.actionsfrom all the results are collected inbear_actionsand it is passed toget_default_actionsfunction. Afterget_default_actionsreturns actions to autoapply we loop over the results and to apply these actions wherever applicable.
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 an 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:
# If action is in `ACTIONS` then action is class.
# Otherwise if action is in `bear_actions` then action is
# object.
if action in ACTIONS:
action = action()
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_resultsget_default_actionfunction needs to be modified to get default actions frombears_actionsalso.
# 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- Auto applying actions specific to bears is same as auto-applying
predefined actions. Users just need to add
default_actions = SomeBear: SomeBearActionin coafile to autoapplySomeBearActionon a result whose origin isSomeBear.
- The above changes will now allow bears to define their own actions and user can apply these actions interactively or by default.
- While writing any bear specific actions user must implement
is_applicableandapplymethod with correct logic. User can also add ainitmethod to pass the necessary data if required. - Some ideas for actions which can be implemented for GitCommitBear
are:
EditCommitMessageAction- Allows to edit commit message.AddNewlineAction- Adds a newline between shortlog and body of message.FixLinelengthAction- Fixes the line length of shortlog are body if greater that specified limit.
EditCommitMessageActionis an action specific toGitCommitBear. On applying this action, an editor will open up in which user can edit the commit message of the HEAD commit.- Implementation of
EditCommitMessageAction
import subprocess
from coalib.results.result_actions.ResultAction import ResultAction
def git(*args):
return subprocess.check_call(['git'] + list(args))
class EditCommitMessageAction(ResultAction):
SUCCESS_MESSAGE = 'Commit message edited successfully.'
@staticmethod
def is_applicable(result,
original_file_dict,
file_diff_dict,
applied_actions=()):
return True
def apply(self, result, original_file_dict, file_diff_dict):
"""
Edit (C)ommit Message [Note: This may rewrite your commit history]
"""
git('commit', '-o', '--amend')
return file_diff_dictAddNewlineActionis an action specific toGitCommitBear. WheneverGitCommitBeardetects that there is no newline between shortlog and body of the commit message it will yield aResultand passAddNewlineActionas an argument.
yield Result(self,
message,
actions=[EditCommitMessageAction(),
AddNewlineAction()])- Implementation of
AddNewlineAction
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
def is_applicable(self,
result,
original_file_dict,
file_diff_dict,
applied_actions=()):
# When `EditCommitMessageAction` or `AddNewlineAction` is
# applied once, then we need to retrieve commit message once
# again and check if action is still applicable or not.
new_message, _ = run_shell_command('git log -1 --pretty=%B')
new_message = new_message.rstrip('\n')
pos = new_message.find('\n')
self.shortlog = new_message[:pos] if pos != -1 else new_message
self.body = new_message[pos+1:] if pos != -1 else ''
if self.body[0] != '\n':
return True
else:
return False
def apply(self, result, original_file_dict, file_diff_dict, **kwargs):
"""
Add New(L)ine [Note: This may rewrite your commit history]
"""
new_commit_message = '{}\n\n{}'.format(self.shortlog, self.body)
command = 'git commit -o --amend -m "{}"'.format(new_commit_message)
stdout, err = run_shell_command(command)
return file_diff_dictCurrently bears suggest the patches in form of diffs, to facilitate bears
suggesting multiple patches we add a new attribute to Result class,
alternate_diffs. It is a list of alternate patches suggested by the bear.
For each alternate patch we add an AlternatePatchAction to list of
actions.
initmethod andfrom_valuesmethod are modified and a new parameter,alternate_diffsis added. It is a list of dictionaries.
class Result:
# A new parameter `alternate_diffs` 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 = [],
alternate_diffs: (list,None) = None):
# A new attribute `alternate_diffs` is added
self.alternate_diffs = alternate_diffs
# A new parameter `alternate_diffs` 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 = [],
alternate_diffs: (list,None) = None):
# Passing an optional argument `alternate_diffs`
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,
alternate_diffs=alternate_diffs)AlternatePatchActionobject holds the alternate patch corresponding to it in adiffsattribute.- The basic idea is to swaps the values of
result.diffsandself.diffsand then applyShowPatchAction. This will show the alternate patch to the user. After this user choosesApplyPatchActionthen changes made are corresponding to the alternate patch.
class AlternatePatchAction(ResultAction):
SUCCESS_MESSAGE = 'Displayed patch successfully.'
def __init__(self, diffs):
self.diffs = diffs
def is_applicable(self,
result: Result,
original_file_dict,
file_diff_dict,
applied_actions=()):
return 'ApplyPatchAction' not in applied_actions
def apply(self,
result,
original_file_dict,
file_diff_dict,
no_color: bool = False):
self.diffs, result.diffs = result.diffs, self.diffs
return ShowPatchAction().apply(result,
original_file_dict,
file_diff_dict,
no_color=no_color)- A new function
get_alternate_patch_actionis defined. It takes a result object as a parameter and returns a tuple ofAlternatePatchActioninstances, each corresponding to an alternative patch.
def get_alternate_patch_actions(result):
"""
Returns a tuple of AlternatePatchAction instances, each corresponding
to an alternate_diff in result.alternate_diffs
"""
alternate_patch_actions = []
if result.alternate_diffs is not None:
for alternate_diff in result.alternate_diffs:
alternate_patch_actions.append(
AlternatePatchAction(alternate_diff))
return tuple(alternate_patch_actions)- This function is called in
acquire_actions_and_applymethod. The tuple returned byget_alternate_patch_actionsis added to the tuple ofcli_actions.
def acquire_actions_and_apply(..)
cli_actions = CLI_ACTIONS if cli_actions is None else cli_actions
cli_actions += get_alternate_patch_actions(result)These changes will now allow bears to suggest multiple patches.