From 7f8a10830de298e4e1a16b9138ff251edbe16157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Pal=C3=A1cios?= Date: Thu, 3 Aug 2017 14:02:23 -0300 Subject: [PATCH] repl: improved ObjectFilenameCompleter completions ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completions are now sorted by file type (first directories, than symbolic links and, finally, ordinary files). Also, we have added a slash to directories completions so it works nicely with the new enter key behaviour. Signed-off-by: Pablo Palácios --- tests/repl/test_completers.py | 60 +++++++++++++++++++++++++ uhu/repl/completers.py | 85 ++++++++++++++++++++++++++++++++--- uhu/repl/helpers.py | 2 +- 3 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 tests/repl/test_completers.py diff --git a/tests/repl/test_completers.py b/tests/repl/test_completers.py new file mode 100644 index 0000000..2595da5 --- /dev/null +++ b/tests/repl/test_completers.py @@ -0,0 +1,60 @@ +# Copyright (C) 2017 O.S. Systems Software LTDA. +# SPDX-License-Identifier: GPL-2.0 + +import os +import shutil +import tempfile +import unittest +from unittest.mock import Mock, patch + +from uhu.repl.completers import ObjectFilenameCompleter + + +class ObjectFilenameCompleterTestCase(unittest.TestCase): + + def test_can_set_completions_in_the_right_order(self): + # creates a base dir + base_dir = tempfile.mkdtemp(prefix='uh-completer-test-') + self.addCleanup(shutil.rmtree, base_dir) + os.chdir(base_dir) + + # Set up some files + _, f1 = tempfile.mkstemp(prefix='file-1', dir=base_dir) + _, f2 = tempfile.mkstemp(prefix='file-2', dir=base_dir) + + # Set up some dirs + dir1 = tempfile.mkdtemp(prefix='dir-1', dir=base_dir) + dir2 = tempfile.mkdtemp(prefix='dir-2', dir=base_dir) + + # Set up some links + os.symlink(__file__, os.path.join(base_dir, 'sym1')) + os.symlink(__file__, os.path.join(base_dir, 'sym2')) + + # Set up prompt toolkit document mock + document = Mock() + document.text_before_cursor = '' + + # Set up completer + completer = ObjectFilenameCompleter() + + # Start the test! + expected = [dir1, dir2, 'sym1', 'sym2', f1, f2] + completions = completer.get_completions(document, None) + observed = [completion.text for completion in completions] + self.assertEqual(len(observed), len(expected)) + + def test_can_handle_files_which_do_not_exist(self): + document = Mock() + document.text_before_cursor = 'invalid-path/invalid-file' + completer = ObjectFilenameCompleter() + completions = list(completer.get_completions(document, None)) + self.assertEqual(len(completions), 0) + + @patch('uhu.repl.completers.os') + def test_can_handle_any_os_error(self, mock): + mock.path.dirname.side_effect = OSError + document = Mock() + document.text_before_cursor = '' + completer = ObjectFilenameCompleter() + completions = completer.get_completions(document, None) + self.assertIsNone(completions) diff --git a/uhu/repl/completers.py b/uhu/repl/completers.py index 072a3dd..0daba8b 100644 --- a/uhu/repl/completers.py +++ b/uhu/repl/completers.py @@ -3,16 +3,89 @@ import os -from prompt_toolkit.contrib.completers import PathCompleter, WordCompleter +from prompt_toolkit.completion import Completer, Completion +from prompt_toolkit.contrib.completers import WordCompleter from ..core.object import Modes -# pylint: disable=too-few-public-methods -class ObjectFilenameCompleter(PathCompleter): - - def __init__(self): - super().__init__(file_filter=os.path.isfile) +class ObjectFilenameCompleter(Completer): + + DIR = 0 + LINK = 1 + FILE = 2 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.value = None + self.directory = None + self.prefix = None + self.filenames = None + + def get_completions(self, document, complete_event): + try: + self.normalize_value(document) + self.set_base_directory() + self.set_base_filename() + self.set_filenames() + return self.all_completions() + except OSError: + pass + + def normalize_value(self, document): + self.value = document.text_before_cursor + + def set_base_directory(self): + dirname = os.path.dirname(self.value) + if dirname: + self.directory = os.path.dirname(os.path.join('.', self.value)) + else: + self.directory = '.' + + def set_base_filename(self): + self.prefix = os.path.basename(self.value) + + def set_filenames(self): + try: + names = sorted([(fn, self._get_sort_key(fn)) + for fn in os.listdir(self.directory) + if fn.startswith(self.prefix)]) + except FileNotFoundError: + names = [] + self.filenames = sorted(names, key=lambda v: v[1]) + + def all_completions(self): + for filename, file_type in self.filenames: + yield self.set_completion(filename, file_type) + + def set_completion(self, filename, file_type): + completion = self._set_completion(file_type, filename) + kwargs = self._set_completion_kwargs(file_type, filename) + return Completion(completion, **kwargs) + + def _get_sort_key(self, fn): + if os.path.isdir(fn): + return self.DIR + if os.path.islink(fn): + return self.LINK + return self.FILE + + def _set_completion(self, file_type, filename): + completion = filename[len(self.prefix):] + if file_type == self.DIR: + completion += '/' + return completion + + def _set_completion_kwargs(self, file_type, filename): + kwargs = { + 'display': filename, + 'start_position': 0, + } + if file_type == self.DIR: + kwargs['display'] = '{}/'.format(filename) + elif file_type == self.LINK: + kwargs['display'] = '{} (symbolic link)'.format(filename) + return kwargs # pylint: disable=too-few-public-methods diff --git a/uhu/repl/helpers.py b/uhu/repl/helpers.py index 1668b24..8090402 100644 --- a/uhu/repl/helpers.py +++ b/uhu/repl/helpers.py @@ -31,7 +31,7 @@ @manager.registry.add_binding(Keys.ControlD) -def ctrl_d(event): +def ctrl_d(_): """Ctrl D quits appliaction returning 0 to sys.""" sys.exit(0)