From ead45e2e87db550095bfe61afcfd93e8c470ab59 Mon Sep 17 00:00:00 2001 From: Aaron Luna Date: Tue, 20 Oct 2020 23:37:14 -0700 Subject: [PATCH 1/4] add wrap_text module - wrap_text function formats long strings for display in console --- bullet/wrap_text.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 bullet/wrap_text.py diff --git a/bullet/wrap_text.py b/bullet/wrap_text.py new file mode 100644 index 0000000..0585119 --- /dev/null +++ b/bullet/wrap_text.py @@ -0,0 +1,34 @@ +import re + +WORD_REGEX = re.compile(r"\s?(?P\b\w+\b)\s?") + + +def wrap_text(s: str, max_len: int): + """Wrap text at word boundaries. + Args: + s (str): The string to be wrapped. + max_len (int): Maximum length of each substring + Returns: + str: String that will display a multiline string where the length of + each line is less than or equal to max_len. Wrapping will not + occur in the middle of a word for prettier output. + """ + substrings = [] + while True: + if len(s) <= max_len: + substrings.append(s) + break + (wrapped, s) = _wrap_string(s, max_len) + substrings.append(wrapped) + return "\n".join(substrings) + + +def _wrap_string(s, max_len): + last_word_boundary = max_len + for match in WORD_REGEX.finditer(s): + if match.end("word") > max_len: + break + last_word_boundary = match.end("word") + 1 + wrapped = s[:last_word_boundary] + s = s[last_word_boundary:].strip() + return (wrapped, s) From 407b4731cf4a4f2c54322739f6ce94603d805dd5 Mon Sep 17 00:00:00 2001 From: Aaron Luna Date: Tue, 20 Oct 2020 23:39:57 -0700 Subject: [PATCH 2/4] add Date prompt for datetime values --- bullet/__init__.py | 3 ++- bullet/client.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ setup.py | 5 +++-- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/bullet/__init__.py b/bullet/__init__.py index 708138b..420690a 100644 --- a/bullet/__init__.py +++ b/bullet/__init__.py @@ -6,4 +6,5 @@ from .client import Numbers from .client import VerticalPrompt from .client import SlidePrompt -from .client import ScrollBar \ No newline at end of file +from .client import ScrollBar +from .client import Date diff --git a/bullet/client.py b/bullet/client.py index be3b061..c3bfe8d 100644 --- a/bullet/client.py +++ b/bullet/client.py @@ -1,5 +1,10 @@ import sys +from datetime import datetime + +from dateutil import parser + from .charDef import * +from .wrap_text import wrap_text from . import colors from . import utils from . import cursor @@ -733,3 +738,43 @@ def launch(self): utils.clearConsoleUp(d + 1) utils.moveCursorDown(1) return self.result + +class Date(Input): + """ + Custom Input element that attempts to parse the value provided by the user as a date object. + Date can be provided in any format recognized by dateutil.parser. + """ + + def __init__( + self, + prompt: str, + default: datetime = None, + str_format: str = "%m/%d/%Y", + indent: int = 0, + word_color: str = colors.foreground["default"], + strip: bool = False, + ): + if default: + default = default.strftime(str_format) + super().__init__(prompt, default, indent, word_color, strip, "") + + def launch(self): + while True: + result = super().launch() + if not result: + continue + try: + parsed_date = parser.parse(result) + return parsed_date + except ValueError: + error = ( + "Error! '" + result + "' could not be parsed as a valid date.\n" + ) + help = ( + "You can use any format recognized by dateutil.parser. For example, all of " + "the strings below are valid ways to represent the same date:\n" + ) + examples = '\n"2018-5-13" -or- "05/13/2018" -or- "May 13 2018"\n' + utils.cprint(error, color=colors.bright(colors.foreground["red"])) + utils.cprint(wrap_text(help, max_len=70), color=colors.foreground["red"]) + utils.cprint(examples, color=colors.foreground["red"]) diff --git a/setup.py b/setup.py index fae0e27..a0159aa 100644 --- a/setup.py +++ b/setup.py @@ -11,5 +11,6 @@ author='Mckinsey666', license='MIT', packages=find_packages(), - python_requires=">=3.6" -) \ No newline at end of file + python_requires=">=3.6", + install_requires=["python-dateutil"], +) From b56b23686cbb0dc0794988420dad285f0d5eee11 Mon Sep 17 00:00:00 2001 From: Aaron Luna Date: Fri, 23 Oct 2020 12:19:18 -0700 Subject: [PATCH 3/4] improved doc strings, minor refactorings --- bullet/client.py | 39 +++++++++++++++++++++++---------------- bullet/wrap_text.py | 11 ++++++----- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/bullet/client.py b/bullet/client.py index c3bfe8d..f29bba9 100644 --- a/bullet/client.py +++ b/bullet/client.py @@ -1,7 +1,7 @@ import sys -from datetime import datetime +from datetime import date -from dateutil import parser +from dateutil import parser as date_parser from .charDef import * from .wrap_text import wrap_text @@ -740,23 +740,32 @@ def launch(self): return self.result class Date(Input): - """ - Custom Input element that attempts to parse the value provided by the user as a date object. - Date can be provided in any format recognized by dateutil.parser. - """ + ''' Prompt user for a `date` value until successfully parsed. + + String provided by user can be provided in any format recognized by + `dateutil.parser`. + + Args: + prompt (str): Required. Text to display to user before input prompt. + default (date): Optional. Default `date` value if user provides no + input. + format_str (str): Format string used to display default value, + defaults to '%m/%d/%Y' + indent (int): Distance between left-boundary and start of prompt. + word_color (str): Optional. The color of the prompt and user input. + ''' def __init__( self, prompt: str, - default: datetime = None, - str_format: str = "%m/%d/%Y", + default: date = None, + format_str: str = "%m/%d/%Y", indent: int = 0, word_color: str = colors.foreground["default"], - strip: bool = False, ): if default: - default = default.strftime(str_format) - super().__init__(prompt, default, indent, word_color, strip, "") + default = default.strftime(format_str) + super().__init__(prompt, default=default, indent=indent, word_color=word_color) def launch(self): while True: @@ -764,12 +773,10 @@ def launch(self): if not result: continue try: - parsed_date = parser.parse(result) - return parsed_date + date = date_parser.parse(result) + return date.date() except ValueError: - error = ( - "Error! '" + result + "' could not be parsed as a valid date.\n" - ) + error = f"Error! '{result}' could not be parsed as a valid date.\n" help = ( "You can use any format recognized by dateutil.parser. For example, all of " "the strings below are valid ways to represent the same date:\n" diff --git a/bullet/wrap_text.py b/bullet/wrap_text.py index 0585119..28f3f7e 100644 --- a/bullet/wrap_text.py +++ b/bullet/wrap_text.py @@ -4,15 +4,16 @@ def wrap_text(s: str, max_len: int): - """Wrap text at word boundaries. + ''' Wrap text at word boundaries. + Args: s (str): The string to be wrapped. max_len (int): Maximum length of each substring Returns: - str: String that will display a multiline string where the length of - each line is less than or equal to max_len. Wrapping will not - occur in the middle of a word for prettier output. - """ + str: Multiline string where the length of each line is less than or + equal to `max_len`. Wrapping will not occur in the middle of a + word for prettier output. + ''' substrings = [] while True: if len(s) <= max_len: From cebd2d78ffe9df783236047870da8bba4110a2ad Mon Sep 17 00:00:00 2001 From: Aaron Luna Date: Fri, 23 Oct 2020 12:24:28 -0700 Subject: [PATCH 4/4] Updated DOCUMENTATION.md to include Date prompt --- DOCUMENTATION.md | 60 +++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 3b02c5c..7f46130 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -25,12 +25,13 @@ - [Using `YesNo` Object](#topic_10) - [Using `Password` Object](#topic_11) - [Using `Numbers` Object](#topic_12) - - [Using Prompt Objects](#topic_13) - - [Using `VerticalPrompt` Object](#topic_14) - - [Using `SlidePrompt` Object](#topic_15) - - [Using `ScrollBar` Object](#topic_16) -- [More Customization: Extending Existing Prompts](#topic_17) - - [A List of Default Keyboard Events](#topic_18) + - [Using `Date` Object](#topic_13) + - [Using Prompt Objects](#topic_14) + - [Using `VerticalPrompt` Object](#topic_15) + - [Using `SlidePrompt` Object](#topic_16) + - [Using `ScrollBar` Object](#topic_17) +- [More Customization: Extending Existing Prompts](#topic_18) + - [A List of Default Keyboard Events](#topic_19) # General @@ -144,10 +145,17 @@ client = Bullet(**styles.Greece) - Non-numeric values will be guarded, and the user will be asked to re-enter. - Define `type` to cast return value. For example, `type = float`, will cast return value to `float`. -## ⌨️ Using `Prompt` Objects +## ⌨️ Using `Date` Objects +> Enter date values +- Values will be [parsed with `dateutil.parser`](https://dateutil.readthedocs.io/en/stable/parser.html), which is capable of handling strings in many different formats (e.g., "2020-8-21", "08/21/2020", or "Aug 21 2020" would all be parsed as the same date). +- If the value provided cannot be parsed, the user will be asked to re-enter. +- Returns a `datetime.date` object +- `format_str: str`: [Format string](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) used to display default value, defaults to `%m/%d/%Y` + +## ⌨️ Using `Prompt` Objects > Wrapping it all up. -### Using `VerticalPrompt` Object +### Using `VerticalPrompt` Object - Stack `bullet` UI components into one vertically-rendered prompt. - Returns a list of tuples `(prompt, result)`. - `spacing`: number of lines between adjacent UI components. @@ -169,20 +177,20 @@ cli = VerticalPrompt( result = cli.launch() ``` -### Using `SlidePrompt` Object +### Using `SlidePrompt` Object - Link `bullet` UI components into a multi-stage prompt. Previous prompts will be cleared upon entering the next stage. - Returns a list of tuples `(prompt, result)`. > For `Prompt` ojects, call `summarize()` after launching the prompt to print out user input. -## ⌨️ Using `ScrollBar` Object +## ⌨️ Using `ScrollBar` Object > **Enhanced `Bullet`**: Too many items? It's OK! - `pointer`: points to item currently selected. - `up_indicator`, `down_indicator`: indicators shown in first and last row of the rendered items. - `height`: maximum items rendered on terminal. - For example, your can have 100 choices (`len(choices) = 100`) but define `height = 5`. -# More Customization: Extending Existing Prompts +# More Customization: Extending Existing Prompts > See `./examples/check.py` for the big picture of what's going on. In `bullet`, you can easily inherit a base class (existing `bullet` objects) and create your customized prompt. This is done by introducing the `keyhandler` module to register user-defined keyboard events. @@ -196,22 +204,22 @@ def accept(self): # do some validation checks: chosen items >= 1 and <= 3. ``` Note that `accept()` is the method for **all** prompts to return user input. The binded keyboard event by default is `NEWLINE_KEY` pressed. -## A List of Default Keyboard Events +## A List of Default Keyboard Events > See `./bullet/charDef.py` - `LINE_BEGIN_KEY` : Ctrl + H -- `LINE_END_KEY`: Ctrl + E -- `TAB_KEY` +- `LINE_END_KEY`: Ctrl + E +- `TAB_KEY` - `NEWLINE_KEY`: Enter -- `ESC_KEY` -- `BACK_SPACE_KEY` -- `ARROW_UP_KEY` -- `ARROW_DOWN_KEY` -- `ARROW_RIGHT_KEY` -- `ARROW_LEFT_KEY` -- `INSERT_KEY` -- `DELETE_KEY` -- `END_KEY` -- `PG_UP_KEY` -- `PG_DOWN_KEY` +- `ESC_KEY` +- `BACK_SPACE_KEY` +- `ARROW_UP_KEY` +- `ARROW_DOWN_KEY` +- `ARROW_RIGHT_KEY` +- `ARROW_LEFT_KEY` +- `INSERT_KEY` +- `DELETE_KEY` +- `END_KEY` +- `PG_UP_KEY` +- `PG_DOWN_KEY` - `SPACE_CHAR` -- `INTERRUPT_KEY`: Ctrl + C \ No newline at end of file +- `INTERRUPT_KEY`: Ctrl + C