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_values
method of the Result class are to be changed. While yielding Result object a new optional parameteractions
can 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)
ConsoleInteraction
module needs to be modified to add the actions inresult.actions
to the list of predefined actions when asking user for an action to apply- For this
acquire_actions_and_apply
function 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
Processing
module. autoapply_actions
needs to be modified.result.actions
from all the results are collected inbear_actions
and it is passed toget_default_actions
function. Afterget_default_actions
returns 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_results
get_default_action
function needs to be modified to get default actions frombears_actions
also.
# 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: SomeBearAction
in coafile to autoapplySomeBearAction
on 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_applicable
andapply
method with correct logic. User can also add ainit
method 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.
EditCommitMessageAction
is 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_dict
AddNewlineAction
is an action specific toGitCommitBear
. WheneverGitCommitBear
detects that there is no newline between shortlog and body of the commit message it will yield aResult
and passAddNewlineAction
as 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_dict
Currently 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.
init
method andfrom_values
method are modified and a new parameter,alternate_diffs
is 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)
AlternatePatchAction
object holds the alternate patch corresponding to it in adiffs
attribute.- The basic idea is to swaps the values of
result.diffs
andself.diffs
and then applyShowPatchAction
. This will show the alternate patch to the user. After this user choosesApplyPatchAction
then 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_action
is defined. It takes a result object as a parameter and returns a tuple ofAlternatePatchAction
instances, 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_apply
method. The tuple returned byget_alternate_patch_actions
is 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.