diff --git a/cliglue/autocomplete/autocomplete.py b/cliglue/autocomplete/autocomplete.py index 41e19b6..e1b5dd7 100644 --- a/cliglue/autocomplete/autocomplete.py +++ b/cliglue/autocomplete/autocomplete.py @@ -73,14 +73,15 @@ def _find_available_completions(rules: List[CliRule], args: List[str], current_w for rule in parameters: for keyword in rule.keywords: if previous == keyword: - possible_choices: List[str] = generate_value_choices(rule) + possible_choices: List[str] = generate_value_choices(rule, current=current_word) return possible_choices # "--param=value" autocompletion for rule in parameters: for keyword in rule.keywords: if current_word.startswith(keyword + '='): - possible_choices: List[str] = list(map(lambda c: keyword + '=' + c, generate_value_choices(rule))) + possible_choices: List[str] = list(map(lambda c: keyword + '=' + c, + generate_value_choices(rule, current=current_word))) return possible_choices completions: List[str] = [] @@ -102,10 +103,10 @@ def _find_available_completions(rules: List[CliRule], args: List[str], current_w # positional arguments for rule in pos_arguments: - possible_choices: List[str] = generate_value_choices(rule) + possible_choices: List[str] = generate_value_choices(rule, current=current_word) completions.extend(possible_choices) for rule in many_args: - possible_choices: List[str] = generate_value_choices(rule) + possible_choices: List[str] = generate_value_choices(rule, current=current_word) completions.extend(possible_choices) return completions diff --git a/cliglue/autocomplete/completers.py b/cliglue/autocomplete/completers.py deleted file mode 100644 index 09de983..0000000 --- a/cliglue/autocomplete/completers.py +++ /dev/null @@ -1,11 +0,0 @@ -import os - - -def file_completer(): - names = [] - for file in [f for f in os.listdir('.')]: - if os.path.isdir(file): - names.append(f'{file}/') - else: - names.append(file) - return sorted(names) diff --git a/cliglue/builder/typedef.py b/cliglue/builder/typedef.py index 3e112ce..21763af 100644 --- a/cliglue/builder/typedef.py +++ b/cliglue/builder/typedef.py @@ -1,5 +1,5 @@ -from typing import Union, Callable, Iterable, List, Type, Any +from typing import Union, Callable, Iterable, List, Type, Any, Optional Action = Callable[..., None] -ChoiceProvider = Union[Iterable[Any], Callable[..., List[Any]]] +ChoiceProvider = Union[Iterable[Any], Callable[[Optional[str]], List[Any]]] TypeOrParser = Union[Type, Callable[[str], Any]] diff --git a/cliglue/completers/__init__.py b/cliglue/completers/__init__.py new file mode 100644 index 0000000..ef3d0c0 --- /dev/null +++ b/cliglue/completers/__init__.py @@ -0,0 +1 @@ +from .file import file_completer diff --git a/cliglue/completers/file.py b/cliglue/completers/file.py new file mode 100644 index 0000000..fa871fc --- /dev/null +++ b/cliglue/completers/file.py @@ -0,0 +1,30 @@ +import os +from typing import Optional, Tuple + + +def file_completer(current: Optional[str]): + listdir, prefixdir = _current_listing_dir(current) + names = [] + for file in os.listdir(listdir): + filepath = f'{prefixdir}{file}' + if os.path.isdir(filepath): + # prevent from immediate resolving + names.append(f'{filepath}/') + names.append(f'{filepath}/ ') + else: + names.append(filepath) + return sorted(names) + + +def _current_listing_dir(current: Optional[str]) -> Tuple[str, str]: + if not current: + return '.', '' + + current_path, current_node = os.path.split(current) + if not current_path or current_path == '.': + return '.', '' + + if not os.path.isdir(current_path): + return '.', '' + + return current_path, f'{current_path}/' diff --git a/cliglue/parser/value.py b/cliglue/parser/value.py index f45a6d8..a67f5e0 100644 --- a/cliglue/parser/value.py +++ b/cliglue/parser/value.py @@ -1,5 +1,6 @@ +import inspect from collections.abc import Iterable -from typing import List, Any +from typing import List, Any, Optional from cliglue.builder.rule import ValueRule from cliglue.builder.typedef import TypeOrParser @@ -16,7 +17,7 @@ def parse_typed_value(_type: TypeOrParser, arg: str) -> Any: return _type(arg) -def generate_value_choices(rule: ValueRule) -> List[Any]: +def generate_value_choices(rule: ValueRule, current: Optional[str] = None) -> List[Any]: if not rule.choices: return [] elif isinstance(rule.choices, list): @@ -24,4 +25,9 @@ def generate_value_choices(rule: ValueRule) -> List[Any]: elif isinstance(rule.choices, Iterable): return [choice for choice in rule.choices] else: - return list(rule.choices()) + (args, _, _, _, _, _, annotations) = inspect.getfullargspec(rule.choices) + if len(args) >= 1: + results = rule.choices(current=current) + else: + results = rule.choices() + return list(results) diff --git a/cliglue/version.py b/cliglue/version.py index a82b376..72f26f5 100644 --- a/cliglue/version.py +++ b/cliglue/version.py @@ -1 +1 @@ -__version__ = "1.1.1" +__version__ = "1.1.2" diff --git a/tests/autocomplete/test_completers.py b/tests/autocomplete/test_completers.py deleted file mode 100644 index 17fdfd0..0000000 --- a/tests/autocomplete/test_completers.py +++ /dev/null @@ -1,17 +0,0 @@ -from cliglue import * -from cliglue.autocomplete.completers import file_completer -from tests.asserts import MockIO - - -def test_file_completer(): - with MockIO('--autocomplete', '"app "') as mockio: - CliBuilder(reraise_error=True).has( - arguments('f', choices=file_completer), - ).run() - proposals = mockio.stripped().splitlines() - assert '.gitignore' in proposals - assert 'tests/' in proposals - assert 'tests/builder/' not in proposals - assert 'tests/__init__.py' not in proposals - assert '.' not in proposals - assert '..' not in proposals diff --git a/tests/completers/__init__.py b/tests/completers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/completers/test_file_completer.py b/tests/completers/test_file_completer.py new file mode 100644 index 0000000..fa705ff --- /dev/null +++ b/tests/completers/test_file_completer.py @@ -0,0 +1,108 @@ +from pathlib import Path + +from cliglue import * +from cliglue.completers import file_completer +from tests.asserts import MockIO + + +def test_file_completer_empty(): + with MockIO('--autocomplete', '"app "') as mockio: + CliBuilder(reraise_error=True).has( + arguments('f', choices=file_completer), + ).run() + proposals = mockio.stripped().splitlines() + assert '.gitignore' in proposals + assert 'tests/' in proposals + + assert 'tests' not in proposals + assert 'tests/autocomplete/' not in proposals + assert 'tests/__init__.py' not in proposals + assert '.' not in proposals + assert '..' not in proposals + + +def test_file_completer_file(): + with MockIO('--autocomplete', '"app .gitignore"') as mockio: + CliBuilder(reraise_error=True).has( + arguments('f', choices=file_completer), + ).run() + assert mockio.stripped() == '.gitignore' + + +def test_file_completer_dir1(): + with MockIO('--autocomplete', '"app tests"') as mockio: + CliBuilder(reraise_error=True).has( + arguments('f', choices=file_completer), + ).run() + proposals = set(mockio.stripped().splitlines()) + assert proposals == {'tests/'} + + +def test_file_completer_dir_content(): + with MockIO('--autocomplete', '"app tests/"') as mockio: + CliBuilder(reraise_error=True).has( + arguments('f', choices=file_completer), + ).run() + proposals = mockio.stripped().splitlines() + assert 'tests/autocomplete/' in proposals + assert 'tests/__init__.py' in proposals + + assert 'tests/' not in proposals + assert 'tests/autocomplete' not in proposals + assert '.gitignore' not in proposals + assert '.' not in proposals + assert '..' not in proposals + + +def test_file_completer_subdir(): + with MockIO('--autocomplete', '"app tests/autocomplete"') as mockio: + CliBuilder(reraise_error=True).has( + arguments('f', choices=file_completer), + ).run() + proposals = set(mockio.stripped().splitlines()) + assert proposals == {'tests/autocomplete/'} + + +def test_file_completer_subdir_content(): + with MockIO('--autocomplete', '"app tests/autocomplete/"') as mockio: + CliBuilder(reraise_error=True).has( + arguments('f', choices=file_completer), + ).run() + proposals = mockio.stripped().splitlines() + assert 'tests/autocomplete/__init__.py' in proposals + + assert 'tests/autocomplete/' not in proposals + assert 'tests/autocomplete' not in proposals + assert 'tests/__init__.py' not in proposals + assert 'tests/' not in proposals + assert '.gitignore' not in proposals + assert '.' not in proposals + assert '..' not in proposals + + +def test_file_completer_subdir_file(): + with MockIO('--autocomplete', '"app tests/autocomplete/__init__.py"') as mockio: + CliBuilder(reraise_error=True).has( + arguments('f', choices=file_completer), + ).run() + assert mockio.stripped() == 'tests/autocomplete/__init__.py' + + +def test_file_completer_notexisting_dir(): + with MockIO('--autocomplete', '"app tests/there_is_no_dir/__init__.py"') as mockio: + CliBuilder(reraise_error=True).has( + arguments('f', choices=file_completer), + ).run() + assert mockio.stripped() == '' + + +def test_complete_absolute_files(): + Path('/tmp/cliglue_test_autocomplete_425_896').write_text('') + + with MockIO('--autocomplete', '"app /tmp/cliglue_test_autocomplete_425"') as mockio: + CliBuilder(reraise_error=True).has( + arguments('f', choices=file_completer), + ).run() + assert mockio.stripped() == '/tmp/cliglue_test_autocomplete_425_896' + + Path('/tmp/cliglue_test_autocomplete_425_896').unlink()