diff --git a/CHANGELOG.md b/CHANGELOG.md index bebe66b..04614cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - This adds a new `\llm` special command - eg: `\llm "Who is the largest customer based on revenue?"` +### Bug Fixes + +* Fix the windows path shown in prompt to remove escaping. + ### Internal * Change min required python version to 3.9+ diff --git a/litecli/main.py b/litecli/main.py index a9eced3..7e5a817 100644 --- a/litecli/main.py +++ b/litecli/main.py @@ -1,52 +1,50 @@ -from __future__ import unicode_literals -from __future__ import print_function +from __future__ import print_function, unicode_literals +import itertools +import logging import os +import re +import shutil import sys -import traceback -import logging import threading -from time import time +import traceback +from collections import namedtuple from datetime import datetime from io import open -from collections import namedtuple from sqlite3 import OperationalError, sqlite_version -import shutil +from time import time -from cli_helpers.tabular_output import TabularOutputFormatter -from cli_helpers.tabular_output import preprocessors import click import sqlparse +from cli_helpers.tabular_output import TabularOutputFormatter, preprocessors +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.completion import DynamicCompleter -from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode -from prompt_toolkit.shortcuts import PromptSession, CompleteStyle from prompt_toolkit.document import Document +from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.filters import HasFocus, IsDone from prompt_toolkit.formatted_text import ANSI +from prompt_toolkit.history import FileHistory from prompt_toolkit.layout.processors import ( - HighlightMatchingBracketProcessor, ConditionalProcessor, + HighlightMatchingBracketProcessor, ) from prompt_toolkit.lexers import PygmentsLexer -from prompt_toolkit.history import FileHistory -from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +from prompt_toolkit.shortcuts import CompleteStyle, PromptSession -from .packages.special.main import NO_QUERY -from .packages.prompt_utils import confirm, confirm_destructive_query -from .packages import special -from .sqlcompleter import SQLCompleter -from .clitoolbar import create_toolbar_tokens_func -from .clistyle import style_factory, style_factory_output -from .sqlexecute import SQLExecute +from .__init__ import __version__ from .clibuffer import cli_is_multiline +from .clistyle import style_factory, style_factory_output +from .clitoolbar import create_toolbar_tokens_func from .completion_refresher import CompletionRefresher from .config import config_location, ensure_dir_exists, get_config from .key_bindings import cli_bindings from .lexer import LiteCliLexer -from .__init__ import __version__ +from .packages import special from .packages.filepaths import dir_path_exists - -import itertools +from .packages.prompt_utils import confirm, confirm_destructive_query +from .packages.special.main import NO_QUERY +from .sqlcompleter import SQLCompleter +from .sqlexecute import SQLExecute click.disable_unicode_literals_warning = True @@ -758,20 +756,32 @@ def get_completions(self, text, cursor_positition): return self.completer.get_completions(Document(text=text, cursor_position=cursor_positition), None) def get_prompt(self, string): - self.logger.debug("Getting prompt") + self.logger.debug("Getting prompt %r", string) sqlexecute = self.sqlexecute now = datetime.now() - string = string.replace("\\d", sqlexecute.dbname or "(none)") - string = string.replace("\\f", os.path.basename(sqlexecute.dbname or "(none)")) - string = string.replace("\\n", "\n") - string = string.replace("\\D", now.strftime("%a %b %d %H:%M:%S %Y")) - string = string.replace("\\m", now.strftime("%M")) - string = string.replace("\\P", now.strftime("%p")) - string = string.replace("\\R", now.strftime("%H")) - string = string.replace("\\r", now.strftime("%I")) - string = string.replace("\\s", now.strftime("%S")) - string = string.replace("\\_", " ") - return string + + # Prepare the replacements dictionary + replacements = { + r"\d": sqlexecute.dbname or "(none)", + r"\f": os.path.basename(sqlexecute.dbname or "(none)"), + r"\n": "\n", + r"\D": now.strftime("%a %b %d %H:%M:%S %Y"), + r"\m": now.strftime("%M"), + r"\P": now.strftime("%p"), + r"\R": now.strftime("%H"), + r"\r": now.strftime("%I"), + r"\s": now.strftime("%S"), + r"\_": " ", + } + # Compile a regex pattern that matches any of the keys in replacements + pattern = re.compile("|".join(re.escape(key) for key in replacements.keys())) + + # Define the replacement function + def replacer(match): + return replacements[match.group(0)] + + # Perform the substitution + return pattern.sub(replacer, string) def run_query(self, query, new_line=True): """Runs *query*.""" diff --git a/tests/test_main.py b/tests/test_main.py index a8fa4ae..1c24da4 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,6 +2,8 @@ from collections import namedtuple from textwrap import dedent import shutil +from datetime import datetime +from unittest.mock import patch import click from click.testing import CliRunner @@ -267,3 +269,64 @@ def test_startup_commands(executor): ] # implement tests on executions of the startupcommands + + +@patch("litecli.main.datetime") # Adjust if your module path is different +def test_get_prompt(mock_datetime): + # We'll freeze time at 2025-01-20 13:37:42 for comedic effect. + # Because "leet" times call for 13:37! + frozen_time = datetime(2025, 1, 20, 13, 37, 42) + mock_datetime.now.return_value = frozen_time + # Ensure `datetime` class is still accessible for strftime usage + mock_datetime.datetime = datetime + + # Instantiate and connect + lc = LiteCli() + lc.connect("/tmp/litecli_test.db") + + # 1. Test \d => full path to the DB + assert lc.get_prompt(r"\d") == "/tmp/litecli_test.db" + + # 2. Test \f => basename of the DB + # (because "f" stands for "filename", presumably!) + assert lc.get_prompt(r"\f") == "litecli_test.db" + + # 3. Test \_ => single space + assert lc.get_prompt(r"Hello\_World") == "Hello World" + + # 4. Test \n => newline + # Just to be sure we're only inserting a newline, + # we can check length or assert the presence of "\n". + expected = f"Line1{os.linesep}Line2" + assert lc.get_prompt(r"Line1\nLine2") == expected + + # 5. Test date/time placeholders (with frozen time): + # \D => e.g. 'Mon Jan 20 13:37:42 2025' + expected_date_str = frozen_time.strftime("%a %b %d %H:%M:%S %Y") + assert lc.get_prompt(r"\D") == expected_date_str + + # 6. Test \m => minutes + assert lc.get_prompt(r"\m") == "37" + + # 7. Test \P => AM/PM + # 13:37 is PM + assert lc.get_prompt(r"\P") == "PM" + + # 8. Test \R => 24-hour format hour + assert lc.get_prompt(r"\R") == "13" + + # 9. Test \r => 12-hour format hour + # 13:37 is 01 in 12-hour format + assert lc.get_prompt(r"\r") == "01" + + # 10. Test \s => seconds + assert lc.get_prompt(r"\s") == "42" + + # 11. Test when dbname is None => (none) + lc.connect(None) # Simulate no DB connection + assert lc.get_prompt(r"\d") == "(none)" + assert lc.get_prompt(r"\f") == "(none)" + + # 12. Windows path + lc.connect("C:\\Users\\litecli\\litecli_test.db") + assert lc.get_prompt(r"\d") == "C:\\Users\\litecli\\litecli_test.db"