diff --git a/example/basic.py b/example/basic.py index c3cab4f..7031f37 100644 --- a/example/basic.py +++ b/example/basic.py @@ -1,8 +1,8 @@ from pick import pick -title = 'Please choose your favorite programming language: ' -options = ['Java', 'JavaScript', 'Python', 'PHP', 'C++', 'Erlang', 'Haskell'] -selection = pick(options, title, indicator='=>', default_index=2) +title = "Please choose your favorite programming language: " +options = ["Java", "JavaScript", "Python", "PHP", "C++", "Erlang", "Haskell"] +selection = pick(options, title, indicator="=>", default_index=2) assert len(selection) == 1 option, index = selection[0] print(option, index) diff --git a/example/custom.py b/example/custom.py index 7e119ab..9aad0b1 100644 --- a/example/custom.py +++ b/example/custom.py @@ -15,8 +15,9 @@ def print_selection(selection): def go_back(picker): return (None, -1) -title = 'Please choose your favorite programming language: ' -options = ['Java', 'JavaScript', 'Python', 'PHP', 'C++', 'Erlang', 'Haskell'] + +title = "Please choose your favorite programming language: " +options = ["Java", "JavaScript", "Python", "PHP", "C++", "Erlang", "Haskell"] # with type annotation picker: Picker[Tuple[None, int], str] = Picker(options, title) diff --git a/example/options_map_func.py b/example/options_map_func.py index 825fd81..b269eeb 100644 --- a/example/options_map_func.py +++ b/example/options_map_func.py @@ -1,19 +1,22 @@ from pick import pick -title = 'Please choose your favorite fruit: ' +title = "Please choose your favorite fruit: " options = [ - { 'name': 'Apples', 'grow_on': 'trees' }, - { 'name': 'Oranges', 'grow_on': 'trees' }, - { 'name': 'Strawberries', 'grow_on': 'vines' }, - { 'name': 'Grapes', 'grow_on': 'vines' }, + {"name": "Apples", "grow_on": "trees"}, + {"name": "Oranges", "grow_on": "trees"}, + {"name": "Strawberries", "grow_on": "vines"}, + {"name": "Grapes", "grow_on": "vines"}, ] + def get_description_for_display(option): # format the option data for display - return '{0} (grow on {1})'.format(option.get('name'), option.get('grow_on')) + return "{0} (grow on {1})".format(option.get("name"), option.get("grow_on")) -selection = pick(options, title, indicator='=>', options_map_func=get_description_for_display) +selection = pick( + options, title, indicator="=>", options_map_func=get_description_for_display +) assert len(selection) == 1 option, index = selection[0] print(option, index) diff --git a/example/scroll.py b/example/scroll.py index 39e4e97..c23d785 100644 --- a/example/scroll.py +++ b/example/scroll.py @@ -1,7 +1,7 @@ from pick import pick -title = 'Select:' -options = ['foo.bar%s.baz' % x for x in range(1, 71)] +title = "Select:" +options = ["foo.bar%s.baz" % x for x in range(1, 71)] selection = pick(options, title) assert len(selection) == 1 option, index = selection[0] diff --git a/poetry.lock b/poetry.lock index a880740..23c403b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,6 +20,42 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +[[package]] +name = "black" +version = "22.6.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + [[package]] name = "colorama" version = "0.4.5" @@ -99,6 +135,26 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.4.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +test = ["pytest-mock (>=3.6)", "pytest-cov (>=2.7)", "pytest (>=6)", "appdirs (==1.4.4)"] +docs = ["sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.2)", "furo (>=2021.7.5b38)", "Sphinx (>=4)"] + [[package]] name = "pluggy" version = "1.0.0" @@ -209,12 +265,14 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" -python-versions = ">=3.6" -content-hash = "38ea8567579c0d40d741c7293be0e7ed26e73bd668901ceaea45c75da981ce9b" +python-versions = ">=3.6.2" +content-hash = "3c96a05214965e06e36e2007214ce97b6ddf2a044ba8513d53c2304123f6a30e" [metadata.files] atomicwrites = [] attrs = [] +black = [] +click = [] colorama = [] dataclasses = [] importlib-metadata = [] @@ -222,6 +280,8 @@ iniconfig = [] mypy = [] mypy-extensions = [] packaging = [] +pathspec = [] +platformdirs = [] pluggy = [] py = [] pyparsing = [] diff --git a/pyproject.toml b/pyproject.toml index 26b39aa..6d2b97e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,13 +10,14 @@ homepage = "https://github.com/wong2/pick" keywords = ["terminal", "gui"] [tool.poetry.dependencies] -python = ">=3.6" +python = ">=3.6.2" windows-curses = {version = "^2.2.0", platform = "win32"} dataclasses = { version = "^0.8", python = "~3.6" } [tool.poetry.dev-dependencies] pytest = "^6.2.5" mypy = "^0.961" +black = "^22.6.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/src/pick/__init__.py b/src/pick/__init__.py index 98fb9ab..58746c5 100644 --- a/src/pick/__init__.py +++ b/src/pick/__init__.py @@ -2,13 +2,13 @@ from dataclasses import dataclass, field from typing import Generic, Callable, List, Optional, Dict, Union, Tuple, TypeVar -__all__ = ['Picker', 'pick'] +__all__ = ["Picker", "pick"] -KEYS_ENTER = (curses.KEY_ENTER, ord('\n'), ord('\r')) -KEYS_UP = (curses.KEY_UP, ord('k')) -KEYS_DOWN = (curses.KEY_DOWN, ord('j')) -KEYS_SELECT = (curses.KEY_RIGHT, ord(' ')) +KEYS_ENTER = (curses.KEY_ENTER, ord("\n"), ord("\r")) +KEYS_UP = (curses.KEY_UP, ord("k")) +KEYS_DOWN = (curses.KEY_DOWN, ord("j")) +KEYS_SELECT = (curses.KEY_RIGHT, ord(" ")) CUSTOM_HANDLER_RETURN_T = TypeVar("CUSTOM_HANDLER_RETURN_T") KEY_T = int @@ -44,23 +44,23 @@ class Picker(Generic[CUSTOM_HANDLER_RETURN_T, OPTIONS_MAP_VALUE_T]): def __post_init__(self) -> None: if len(self.options) == 0: - raise ValueError('options should not be an empty list') + raise ValueError("options should not be an empty list") if self.default_index >= len(self.options): - raise ValueError('default_index should be less than the length of options') + raise ValueError("default_index should be less than the length of options") if self.multiselect and self.min_selection_count > len(self.options): - raise ValueError('min_selection_count is bigger than the available options, you will not be able to make any selection') + raise ValueError( + "min_selection_count is bigger than the available options, you will not be able to make any selection" + ) if not callable(self.options_map_func): - raise ValueError('options_map_func must be a callable function') + raise ValueError("options_map_func must be a callable function") self.index = self.default_index def register_custom_handler( - self, - key: KEY_T, - func: Callable[["Picker"], CUSTOM_HANDLER_RETURN_T] + self, key: KEY_T, func: Callable[["Picker"], CUSTOM_HANDLER_RETURN_T] ) -> None: self.custom_handlers[key] = func @@ -83,7 +83,7 @@ def mark_index(self) -> None: def get_selected(self) -> List[Tuple[OPTIONS_MAP_VALUE_T, int]]: """return the current selected option as a tuple: (option, index) - or as a list of tuples (in case multiselect==True) + or as a list of tuples (in case multiselect==True) """ if self.multiselect: return_tuples = [] @@ -96,7 +96,7 @@ def get_selected(self) -> List[Tuple[OPTIONS_MAP_VALUE_T, int]]: def get_title_lines(self) -> List[str]: if self.title: - return self.title.split('\n') + [''] + return self.title.split("\n") + [""] return [] def get_option_lines(self) -> Union[List[str], List[Tuple[str, int]]]: @@ -107,14 +107,14 @@ def get_option_lines(self) -> Union[List[str], List[Tuple[str, int]]]: if index == self.index: prefix = self.indicator else: - prefix = len(self.indicator) * ' ' + prefix = len(self.indicator) * " " line: Union[Tuple[str, int], str] if self.multiselect and index in self.all_selected: format = curses.color_pair(1) - line = ('{0} {1}'.format(prefix, option_as_str), format) + line = ("{0} {1}".format(prefix, option_as_str), format) else: - line = '{0} {1}'.format(prefix, option_as_str) + line = "{0} {1}".format(prefix, option_as_str) lines.append(line) # type: ignore[arg-type] return lines @@ -142,19 +142,19 @@ def draw(self, screen) -> None: elif current_line - self.scroll_top > max_rows: self.scroll_top = current_line - max_rows - lines_to_draw = lines[self.scroll_top:self.scroll_top+max_rows] + lines_to_draw = lines[self.scroll_top : self.scroll_top + max_rows] for line in lines_to_draw: if type(line) is tuple: - screen.addnstr(y, x, line[0], max_x-2, line[1]) + screen.addnstr(y, x, line[0], max_x - 2, line[1]) else: - screen.addnstr(y, x, line, max_x-2) + screen.addnstr(y, x, line, max_x - 2) y += 1 screen.refresh() def run_loop( - self, screen + self, screen ) -> Union[List[Tuple[OPTIONS_MAP_VALUE_T, int]], CUSTOM_HANDLER_RETURN_T]: while True: self.draw(screen) @@ -164,7 +164,10 @@ def run_loop( elif c in KEYS_DOWN: self.move_down() elif c in KEYS_ENTER: - if self.multiselect and len(self.all_selected) < self.min_selection_count: + if ( + self.multiselect + and len(self.all_selected) < self.min_selection_count + ): continue return self.get_selected() elif c in KEYS_SELECT and self.multiselect: @@ -194,7 +197,7 @@ def _start( return self.run_loop(screen) def start( - self + self, ) -> Union[List[Tuple[OPTIONS_MAP_VALUE_T, int]], CUSTOM_HANDLER_RETURN_T]: return curses.wrapper(self._start) diff --git a/tests/test_pick.py b/tests/test_pick.py index 32a285a..68201e8 100644 --- a/tests/test_pick.py +++ b/tests/test_pick.py @@ -40,7 +40,9 @@ def test_no_title(): def test_multi_select(): title = "Please choose an option: " options = ["option1", "option2", "option3"] - picker: Picker[str, str] = Picker(options, title, multiselect=True, min_selection_count=1) + picker: Picker[str, str] = Picker( + options, title, multiselect=True, min_selection_count=1 + ) assert picker.get_selected() == [] picker.mark_index() assert picker.get_selected() == [("option1", 0)] @@ -56,7 +58,9 @@ def test_options_map_func(): def get_label(option: Dict[str, str]) -> Optional[str]: return option.get("label") - picker: Picker[str, Dict[str, str]] = Picker(options, title, indicator="*", options_map_func=get_label) + picker: Picker[str, Dict[str, str]] = Picker( + options, title, indicator="*", options_map_func=get_label + ) lines, current_line = picker.get_lines() assert lines == [title, "", "* option1", " option2", " option3"] assert picker.get_selected() == [({"label": "option1"}, 0)]