Skip to content

Commit

Permalink
Merge pull request #12 from giannisdoukas/autocomplete
Browse files Browse the repository at this point in the history
Enable autocomplete for magic commands arguments
  • Loading branch information
giannisdoukas committed Jul 8, 2020
2 parents 3568d97 + 4953e1e commit cf0915e
Show file tree
Hide file tree
Showing 25 changed files with 557 additions and 414 deletions.
58 changes: 45 additions & 13 deletions cwlkernel/AutoCompleteEngine.py
@@ -1,4 +1,5 @@
from typing import Dict, Iterable, Optional
import re
from typing import Dict, Iterable, Optional, Callable, Tuple, List

from pygtrie import CharTrie

Expand All @@ -8,7 +9,8 @@ class AutoCompleteEngine:
AutoCompleteEngine generates suggestions given a users input.
"""

def __init__(self, magic_commands: Optional[Iterable]):
def __init__(self, magic_commands: Optional[Iterable[str]]):
self._magics_args_suggesters: Dict[str, Callable] = {}
self._commands_trie = CharTrie()
if magic_commands is not None:
for magic in magic_commands:
Expand All @@ -23,24 +25,54 @@ def suggest(self, code: str, cursor_pos: int) -> Dict:
'cursor_end': ,
'cursor_start': , }
"""
cursor_end, cursor_start, token = self._parse(code, cursor_pos)
matches = []
cursor_end = cursor_pos
cursor_start = cursor_pos
line_classifier = re.compile(r'(?P<command>^%[ ]+[\w]*)(?P<args>( [\S]*)*)', re.MULTILINE)
for match in line_classifier.finditer(code): # type: re.Match
if match.start('command') <= cursor_pos <= match.end('command'):
new_cursor_pos = cursor_pos - match.span()[0]
code = match.group()
matches, cursor_start, cursor_end = self._suggest_magic_command(code, new_cursor_pos)
cursor_start += match.span()[0]
cursor_end += match.span()[0]
elif match.span()[0] <= cursor_pos <= match.span()[1]:
new_cursor_pos = cursor_pos - match.start('args')
code = match.group('args')
command = match.group('command')[1:].strip()
matches, cursor_start, cursor_end = self._suggest_magics_arguments(command, code, new_cursor_pos)
cursor_start += match.start('args')
cursor_end += match.start('args')
return {
'matches': matches,
'cursor_end': cursor_end,
'cursor_start': cursor_start
}

def _suggest_magic_command(self, code: str, cursor_pos: int) -> Tuple[List[str], int, int]:
cursor_end, cursor_start, token = self._parse_tokens(code, cursor_pos)
if token == '%':
token = ''
try:
matches = list(set(self._commands_trie.values(prefix=token)))
matches = [m for m in set(self._commands_trie.values(prefix=token))]
matches.sort(key=len)
except KeyError:
matches = []
cursor_end = cursor_pos
cursor_start = cursor_pos
return {
'matches': matches,
'cursor_end': cursor_end,
'cursor_start': cursor_start
}
return matches, cursor_start, cursor_end

def _suggest_magics_arguments(self, command: str, code: str, cursor_pos: int) -> Tuple[List[str], int, int]:
"""Stateless command's arguments suggester"""
cursor_end, cursor_start, query_token = self._parse_tokens(code, cursor_pos)
options: List[str] = self._magics_args_suggesters[command](query_token)
return options, cursor_start, cursor_end

def add_magic_commands_suggester(self, magic_name: str, suggester: Callable) -> None:
self._magics_args_suggesters[magic_name] = suggester

@classmethod
def _parse(cls, code, cursor_pos):
def _parse_tokens(cls, code, cursor_pos):
code_length = len(code)
token_ends_at = code.find(" ", cursor_pos)
cursor_end = min(token_ends_at + 1, code_length - 1)
Expand All @@ -55,6 +87,6 @@ def _parse(cls, code, cursor_pos):
token = code[token_starts_at:token_ends_at + 1].strip().upper()
return cursor_end, cursor_start, token

def add_magic_command(self, magic_command: str):
for i in range(1, len(magic_command) + 1):
self._commands_trie[magic_command[-i:].upper()] = magic_command
def add_magic_command(self, magic_command_name: str):
for i in range(1, len(magic_command_name) + 1):
self._commands_trie[magic_command_name[-i:].upper()] = magic_command_name
33 changes: 22 additions & 11 deletions cwlkernel/CWLKernel.py
Expand Up @@ -39,7 +39,7 @@ class CWLKernel(Kernel):
banner = "Common Workflow Language"

_magic_commands: Dict = {}
_auto_complete_engine = AutoCompleteEngine(_magic_commands)
_auto_complete_engine = AutoCompleteEngine(_magic_commands.keys())

def __init__(self, **kwargs):
super().__init__(**kwargs)
Expand Down Expand Up @@ -76,16 +76,27 @@ def workflow_composer(self) -> CWLWorkflow:
def workflow_composer(self, composer=Optional[CWLWorkflow]):
self._workflow_composer = composer

@classmethod
def register_magic(cls, magic: Callable):
"""
Registers magic commands. That method should be used as a decorator to register custom magic commands.
@param magic: The magic command to register
@return: the magic function
"""
cls._magic_commands[magic.__name__] = magic
cls._auto_complete_engine.add_magic_command(magic.__name__)
return magic
class register_magic:
"""Registers magic commands. That method should be used as a decorator to register custom magic commands."""

def __init__(self, magics_name: Optional[str] = None):
self._magics_name = magics_name

def __call__(self, magic: Callable):
magics_name = self._magics_name if self._magics_name is not None else magic.__name__
CWLKernel._magic_commands[magics_name] = magic
CWLKernel._auto_complete_engine.add_magic_command(magic.__name__)
return magic

class register_magics_suggester:
"""Decorator for registering functions for suggesting commands line arguments"""

def __init__(self, magic_command_name: str):
self._magic_command_name = magic_command_name

def __call__(self, suggester):
CWLKernel._auto_complete_engine.add_magic_commands_suggester(self._magic_command_name, suggester)
return suggester

def _set_process_ids(self):
self._cwl_logger.process_id = {
Expand Down
81 changes: 47 additions & 34 deletions cwlkernel/kernel_magics.py
Expand Up @@ -4,6 +4,7 @@
import random
from io import StringIO
from pathlib import Path
from typing import List

from ruamel.yaml import YAML

Expand All @@ -12,15 +13,15 @@
from .cwlrepository.CWLComponent import CWLWorkflow, WorkflowComponentFactory


@CWLKernel.register_magic
def newWorkflowBuild(kernel: CWLKernel, *args):
@CWLKernel.register_magic('newWorkflowBuild')
def new_workflow_build(kernel: CWLKernel, *args):
kernel.send_json_response(kernel.workflow_composer.to_dict())
kernel.workflow_repository.register_tool(kernel.workflow_composer)
kernel.workflow_composer = None


@CWLKernel.register_magic
def newWorkflowAddInput(kernel: CWLKernel, args: str):
@CWLKernel.register_magic('newWorkflowAddInput')
def new_workflow_add_input(kernel: CWLKernel, args: str):
import yaml as y
args = args.splitlines()
step_id, step_in_id = args[0].split()
Expand All @@ -32,8 +33,8 @@ def newWorkflowAddInput(kernel: CWLKernel, args: str):
in_step_id=step_in_id.strip())


@CWLKernel.register_magic
def newWorkflowAddStepIn(kernel: CWLKernel, args: str):
@CWLKernel.register_magic('newWorkflowAddStepIn')
def new_workflow_add_step_in(kernel: CWLKernel, args: str):
args = args.splitlines()
step_in_args = args[0].split()
input_description = '\n'.join(args[1:])
Expand All @@ -43,25 +44,25 @@ def newWorkflowAddStepIn(kernel: CWLKernel, args: str):
kernel.workflow_composer.add_step_in_out(description, input_id, *step_in_args)


@CWLKernel.register_magic
def newWorkflowAddStep(kernel: CWLKernel, ids: str):
@CWLKernel.register_magic('newWorkflowAddStep')
def new_workflow_add_step(kernel: CWLKernel, ids: str):
tool_id, step_id = ids.split()
tool = kernel.workflow_repository.get_by_id(tool_id)
kernel.workflow_composer.add(tool, step_id)


@CWLKernel.register_magic
def newWorkflowAddOutputSource(kernel: CWLKernel, args: str):
@CWLKernel.register_magic('newWorkflowAddOutputSource')
def new_workflow_add_output_source(kernel: CWLKernel, args: str):
reference, type_of = args.split()
kernel.workflow_composer.add_output_source(reference, type_of)


@CWLKernel.register_magic
def newWorkflow(kernel: CWLKernel, workflow_id: str):
@CWLKernel.register_magic('newWorkflow')
def new_workflow(kernel: CWLKernel, workflow_id: str):
kernel.workflow_composer = CWLWorkflow(workflow_id)


@CWLKernel.register_magic
@CWLKernel.register_magic()
def snippet(kernel: CWLKernel, command: str):
"""
Submit a cwl workflow incrementally. Usage:
Expand Down Expand Up @@ -94,29 +95,41 @@ def snippet(kernel: CWLKernel, command: str):
kernel.send_json_response(current_code)


@CWLKernel.register_magic
def execute(kernel: CWLKernel, execute_argument_string: str):
execute_argument_string = execute_argument_string.splitlines()
cwl_id = execute_argument_string[0].strip()
cwl_component_path: Path = kernel.workflow_repository.get_tools_path_by_id(cwl_id)
kernel._set_data('\n'.join(execute_argument_string[1:]))
kernel._execute_workflow(cwl_component_path)
kernel._clear_data()
class ExecutionMagics:

@staticmethod
@CWLKernel.register_magic()
def execute(kernel: CWLKernel, execute_argument_string: str):
execute_argument_string = execute_argument_string.splitlines()
cwl_id = execute_argument_string[0].strip()
cwl_component_path: Path = kernel.workflow_repository.get_tools_path_by_id(cwl_id)
kernel._set_data('\n'.join(execute_argument_string[1:]))
kernel._execute_workflow(cwl_component_path)
kernel._clear_data()

@CWLKernel.register_magic
@staticmethod
@CWLKernel.register_magics_suggester('execute')
def suggest_execution_id(query_token: str, *args, **kwargs) -> List[str]:
return [
command for command in
CWLKernel.instance()._workflow_repository._registry.keys()
if command.upper().startswith(query_token.upper())
]


@CWLKernel.register_magic('displayData')
def display_data(kernel: CWLKernel, data_name: str) -> None:
"""
Display the data generated by workflow.
Usage % display_data [data id]
Usage % displayData [data id]
@param kernel: the kernel instance
@param data_name: the data id
@return None
"""
if not isinstance(data_name, str) or len(data_name.split()) == 0:
kernel.send_error_response(
'ERROR: you must select an output to display. Correct format:\n % display_data [output name]'
'ERROR: you must select an output to display. Correct format:\n % displayData [output name]'
)
return
result = kernel.results_manager.get_last_result_by_id(data_name)
Expand All @@ -128,7 +141,7 @@ def display_data(kernel: CWLKernel, data_name: str) -> None:
kernel.send_response(kernel.iopub_socket, 'stream', {'name': 'stdout', 'text': data})


@CWLKernel.register_magic
@CWLKernel.register_magic('displayDataCSV')
def display_data_csv(kernel: CWLKernel, data_name: str):
import pandas as pd
if not isinstance(data_name, str) or len(data_name.split()) == 0:
Expand All @@ -155,7 +168,7 @@ def display_data_csv(kernel: CWLKernel, data_name: str):
)


@CWLKernel.register_magic
@CWLKernel.register_magic('sampleCSV')
def sample_csv(kernel: CWLKernel, args: str):
import pandas as pd
try:
Expand Down Expand Up @@ -186,7 +199,7 @@ def sample_csv(kernel: CWLKernel, args: str):
)


@CWLKernel.register_magic
@CWLKernel.register_magic('displayDataImage')
def display_data_image(kernel: CWLKernel, data_name: str):
import base64
if not isinstance(data_name, str) or len(data_name.split()) == 0:
Expand Down Expand Up @@ -225,7 +238,7 @@ def display_data_image(kernel: CWLKernel, data_name: str):
)


@CWLKernel.register_magic
@CWLKernel.register_magic()
def logs(kernel: CWLKernel, limit=None):
limit_len = len(limit)
if limit_len == 0:
Expand All @@ -252,7 +265,7 @@ def logs(kernel: CWLKernel, limit=None):
)


@CWLKernel.register_magic
@CWLKernel.register_magic()
def data(kernel: CWLKernel, *args):
"""
Display all the data which are registered in the kernel session.
Expand All @@ -276,8 +289,8 @@ def data(kernel: CWLKernel, *args):
)


@CWLKernel.register_magic
def githubImport(kernel: CWLKernel, url: str):
@CWLKernel.register_magic('githubImport')
def github_import(kernel: CWLKernel, url: str):
cwl_factory = WorkflowComponentFactory()
for cwl_file in kernel._github_resolver.resolve(url):
with open(cwl_file) as f:
Expand All @@ -288,16 +301,16 @@ def githubImport(kernel: CWLKernel, url: str):
{'name': 'stdout', 'text': f"tool '{cwl_component.id}' registered\n"})


@CWLKernel.register_magic
def viewTool(kernel: CWLKernel, workflow_id: str):
@CWLKernel.register_magic('viewTool')
def view_tool(kernel: CWLKernel, workflow_id: str):
workflow = kernel.workflow_repository.__repo__.get_by_id(workflow_id)
if workflow is not None:
kernel.send_json_response(workflow.to_dict())
else:
kernel.send_error_response(f"Tool '{workflow_id}' is not registered")


@CWLKernel.register_magic
@CWLKernel.register_magic()
def magics(kernel: CWLKernel, arg: str):
arg = arg.split()
parser = argparse.ArgumentParser()
Expand Down

0 comments on commit cf0915e

Please sign in to comment.