Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Bug Fixes
Documentation
--------
* Badge color nit in `README.md`.
* Add a `/source --help` option.


Internal
Expand Down
8 changes: 6 additions & 2 deletions mycli/client_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from mycli.packages.ptoolkit.history import FileHistoryWithTimestamp
from mycli.packages.special.main import ArgType, SpecialCommandAlias
from mycli.packages.special.source import (
SOURCE_HELP_ROWS,
parse_source_arguments,
parse_source_filename,
source_special_command_is_safe,
Expand Down Expand Up @@ -290,10 +291,13 @@ def change_db(self, arg: str, **_) -> Generator[SQLResult, None, None]:

def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
try:
filename, allow_special, show_queries, page_output, throttle = parse_source_arguments(arg)
filename, allow_special, show_queries, page_output, throttle, show_help = parse_source_arguments(arg)
except ValueError as error:
yield SQLResult(status=str(error), is_error=True)
return
if show_help:
yield SQLResult(header=['Argument', 'Description'], rows=SOURCE_HELP_ROWS)
return
if page_output:
yield SQLResult(command={'name': 'source_page'})
try:
Expand All @@ -302,7 +306,7 @@ def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
yield SQLResult(status=str(error), is_error=True)
return
if not filename:
yield SQLResult(status="Missing required argument: filename.", is_error=True)
yield SQLResult(status="Missing required argument: filename. See /source --help.", is_error=True)
return

try:
Expand Down
8 changes: 5 additions & 3 deletions mycli/packages/completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from mycli.packages.special.favoritequeries import FAVORITE_SUBCOMMANDS
from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS
from mycli.packages.special.main import parse_special_command
from mycli.packages.special.source import SOURCE_BOOLEAN_OPTIONS, SOURCE_OPTIONS
from mycli.packages.sql_utils import extract_tables, find_prev_keyword, last_word

sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
Expand Down Expand Up @@ -816,8 +817,7 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
'source',
'/source',
]:
source_options = ['--special', '--show', '--page', '--throttle']
source_boolean_options = source_options[:-1]
source_options = list(SOURCE_OPTIONS)
source_arguments = _arg.split()
if not source_arguments:
return [
Expand All @@ -829,10 +829,12 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
argument_index = 0
while argument_index < len(source_arguments):
argument = source_arguments[argument_index]
if argument in source_boolean_options:
if argument in SOURCE_BOOLEAN_OPTIONS:
used_options.add(argument)
argument_index += 1
continue
if argument == '--help':
return []
if argument == '--throttle':
used_options.add(argument)
argument_index += 1
Expand Down
2 changes: 1 addition & 1 deletion mycli/packages/hybrid_redirection.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def find_sql_part(
if SOURCE_COMMAND_PATTERN.match(sql_part):
source_arg_str = SOURCE_COMMAND_PATTERN.sub('', sql_part)
try:
filename, _allow_special, _show_queries, _page_output, _throttle = parse_source_arguments(source_arg_str)
filename, _allow_special, _show_queries, _page_output, _throttle, _show_help = parse_source_arguments(source_arg_str)
filename = parse_source_filename(filename)
except ValueError:
return ''
Expand Down
19 changes: 17 additions & 2 deletions mycli/packages/special/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@
from mycli.packages.special.iocommands import expand_favorite_query

INVALID_SOURCE_FILENAME = 'Source accepts exactly one filename; filenames containing spaces must be quoted.'
SOURCE_BOOLEAN_OPTIONS = ('--special', '--show', '--page')
SOURCE_OPTIONS = (*SOURCE_BOOLEAN_OPTIONS, '--throttle', '--help')
SOURCE_HELP_ROWS = [
('--special', 'Allow supported special /commands in the source file.'),
('--show', 'Show each statement before executing it.'),
('--page', 'Display all source output using the pager.'),
(
'--throttle <float>, --throttle=<float>',
'Seconds to wait between executing statements.',
),
('--help', 'Show this help.'),
('<filename>', 'File containing SQL to execute.'),
]
SOURCE_SAFE_SPECIAL_COMMANDS = frozenset({
'connect',
'fd',
Expand Down Expand Up @@ -85,7 +98,7 @@ def _parse_throttle(value: str) -> float:
return throttle


def parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool, float]:
def parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool, float, bool]:
allow_special = False
show_queries = False
page_output = False
Expand All @@ -98,6 +111,8 @@ def parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool, float]:
show_queries = True
elif arguments[0] == '--page':
page_output = True
elif arguments[0] == '--help':
return '', allow_special, show_queries, page_output, throttle, True
elif arguments[0] == '--throttle':
if len(arguments) != 2:
raise ValueError('Missing value for --throttle.')
Expand All @@ -110,7 +125,7 @@ def parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool, float]:
else:
break
filename = arguments[1] if len(arguments) == 2 else ''
return filename, allow_special, show_queries, page_output, throttle
return filename, allow_special, show_queries, page_output, throttle, False


def parse_source_filename(filename: str) -> str:
Expand Down
39 changes: 36 additions & 3 deletions test/pytests/test_client_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,9 @@ def test_change_db_without_argument_reports_error(monkeypatch: pytest.MonkeyPatc
def test_execute_from_file_requires_filename() -> None:
client = DummyClient()

assert list(client.execute_from_file('')) == [SQLResult(status='Missing required argument: filename.', is_error=True)]
assert list(client.execute_from_file('')) == [
SQLResult(status='Missing required argument: filename. See /source --help.', is_error=True)
]


def test_execute_from_file_reports_open_errors() -> None:
Expand Down Expand Up @@ -648,6 +650,35 @@ def test_execute_from_file_runs_file_query(tmp_path: Path) -> None:
assert client.sqlexecute.runs == ['select 1;']


def test_execute_from_file_help_is_terminal(monkeypatch: pytest.MonkeyPatch) -> None:
client = DummyClient()
client.sqlexecute = FakeSQLExecute()
opened_paths: list[str] = []
sleep_calls: list[float] = []
monkeypatch.setattr(client_commands, 'open', lambda path: opened_paths.append(path), raising=False)
monkeypatch.setattr(client_commands.time, 'sleep', lambda seconds: sleep_calls.append(seconds))

assert list(client.execute_from_file('--page --help ignored.sql')) == [
SQLResult(
header=['Argument', 'Description'],
rows=[
('--special', 'Allow supported special /commands in the source file.'),
('--show', 'Show each statement before executing it.'),
('--page', 'Display all source output using the pager.'),
(
'--throttle <float>, --throttle=<float>',
'Seconds to wait between executing statements.',
),
('--help', 'Show this help.'),
('<filename>', 'File containing SQL to execute.'),
],
)
]
assert opened_paths == []
assert client.sqlexecute.runs == []
assert sleep_calls == []


def test_execute_from_file_throttles_between_executed_statements(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
client = DummyClient()
sql_file = tmp_path / 'query.sql'
Expand Down Expand Up @@ -775,7 +806,7 @@ def open_file(path: str) -> IteratedFile:
def test_execute_from_file_reports_missing_filename_after_options(options: str) -> None:
client = DummyClient()

expected = [SQLResult(status='Missing required argument: filename.', is_error=True)]
expected = [SQLResult(status='Missing required argument: filename. See /source --help.', is_error=True)]
if '--page' in options:
expected.insert(0, SQLResult(command={'name': 'source_page'}))
assert list(client.execute_from_file(options)) == expected
Expand All @@ -802,7 +833,9 @@ def test_execute_from_file_pages_invalid_filename_error() -> None:
def test_execute_from_file_treats_empty_quotes_as_missing_filename() -> None:
client = DummyClient()

assert list(client.execute_from_file('""')) == [SQLResult(status='Missing required argument: filename.', is_error=True)]
assert list(client.execute_from_file('""')) == [
SQLResult(status='Missing required argument: filename. See /source --help.', is_error=True)
]


def test_execute_from_file_runs_permitted_special_commands(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
Expand Down
34 changes: 23 additions & 11 deletions test/pytests/test_completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ def test_suggest_type_handles_parser_results_shorter_than_cursor(monkeypatch):
[
{
'type': 'special_subcommand',
'subcommands': ['--special', '--show', '--page', '--throttle'],
'subcommands': ['--special', '--show', '--page', '--throttle', '--help'],
},
SOURCE_FILE_SUGGESTION,
],
Expand All @@ -904,7 +904,7 @@ def test_suggest_type_handles_parser_results_shorter_than_cursor(monkeypatch):
[
{
'type': 'special_subcommand',
'subcommands': ['--special', '--show', '--page', '--throttle'],
'subcommands': ['--special', '--show', '--page', '--throttle', '--help'],
},
SOURCE_FILE_SUGGESTION,
],
Expand All @@ -914,56 +914,68 @@ def test_suggest_type_handles_parser_results_shorter_than_cursor(monkeypatch):
[
{
'type': 'special_subcommand',
'subcommands': ['--special', '--show', '--page', '--throttle'],
'subcommands': ['--special', '--show', '--page', '--throttle', '--help'],
}
],
),
('source --special', []),
(
'source --special ',
[
{'type': 'special_subcommand', 'subcommands': ['--show', '--page', '--throttle']},
{'type': 'special_subcommand', 'subcommands': ['--show', '--page', '--throttle', '--help']},
SOURCE_FILE_SUGGESTION,
],
),
(
'source --special --s',
[{'type': 'special_subcommand', 'subcommands': ['--show', '--page', '--throttle']}],
[{'type': 'special_subcommand', 'subcommands': ['--show', '--page', '--throttle', '--help']}],
),
('source --show', []),
(
'source --show ',
[
{'type': 'special_subcommand', 'subcommands': ['--special', '--page', '--throttle']},
{'type': 'special_subcommand', 'subcommands': ['--special', '--page', '--throttle', '--help']},
SOURCE_FILE_SUGGESTION,
],
),
(
'source --show --special ',
[
{'type': 'special_subcommand', 'subcommands': ['--page', '--throttle']},
{'type': 'special_subcommand', 'subcommands': ['--page', '--throttle', '--help']},
SOURCE_FILE_SUGGESTION,
],
),
(
'source --show --special --page ',
[{'type': 'special_subcommand', 'subcommands': ['--throttle']}, SOURCE_FILE_SUGGESTION],
[{'type': 'special_subcommand', 'subcommands': ['--throttle', '--help']}, SOURCE_FILE_SUGGESTION],
),
(
'source --h',
[
{
'type': 'special_subcommand',
'subcommands': ['--special', '--show', '--page', '--throttle', '--help'],
}
],
),
('source --help', []),
('source --help ', []),
('source --show --help ignored.sql', []),
('source --throttle', []),
('source --throttle ', []),
('source --throttle 0.25', []),
(
'source --throttle 0.25 ',
[
{'type': 'special_subcommand', 'subcommands': ['--special', '--show', '--page']},
{'type': 'special_subcommand', 'subcommands': ['--special', '--show', '--page', '--help']},
SOURCE_FILE_SUGGESTION,
],
),
('source --throttle=0.25', []),
(
'source --throttle=0.25 ',
[
{'type': 'special_subcommand', 'subcommands': ['--special', '--show', '--page']},
{'type': 'special_subcommand', 'subcommands': ['--special', '--show', '--page', '--help']},
SOURCE_FILE_SUGGESTION,
],
),
Expand Down Expand Up @@ -1926,7 +1938,7 @@ def test_source_is_file(expression):
)
suggestions = suggest_type(expression, expression)
assert suggestions == [
{'type': 'special_subcommand', 'subcommands': ['--special', '--show', '--page', '--throttle']},
{'type': 'special_subcommand', 'subcommands': ['--special', '--show', '--page', '--throttle', '--help']},
SOURCE_FILE_SUGGESTION,
]

Expand Down
26 changes: 19 additions & 7 deletions test/pytests/test_smart_completion_public_schema_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,33 +729,45 @@ def dummy_list_path(dir_name):
[
(
'source ',
[('--special', 0), ('--show', 0), ('--page', 0), ('--throttle', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
[
('--special', 0),
('--show', 0),
('--page', 0),
('--throttle', 0),
('--help', 0),
('/', 0),
('~', 0),
('.', 0),
('..', 0),
],
),
('source --s', [('--show', -3), ('--special', -3)]),
('source --h', [('--help', -3)]),
('source --help ', []),
(
'source --special ',
[('--show', 0), ('--page', 0), ('--throttle', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
[('--show', 0), ('--page', 0), ('--throttle', 0), ('--help', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
),
(
'source --show ',
[('--special', 0), ('--page', 0), ('--throttle', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
[('--special', 0), ('--page', 0), ('--throttle', 0), ('--help', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
),
(
'source --special --show ',
[('--page', 0), ('--throttle', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
[('--page', 0), ('--throttle', 0), ('--help', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
),
(
'source --special --show --page ',
[('--throttle', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
[('--throttle', 0), ('--help', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
),
('source --throttle ', []),
(
'source --throttle 0.25 ',
[('--special', 0), ('--show', 0), ('--page', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
[('--special', 0), ('--show', 0), ('--page', 0), ('--help', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
),
(
'source --throttle=0.25 ',
[('--special', 0), ('--show', 0), ('--page', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
[('--special', 0), ('--show', 0), ('--page', 0), ('--help', 0), ('/', 0), ('~', 0), ('.', 0), ('..', 0)],
),
("source /", [("/dir1", -1), ("/file1.sql", -1), ("/file2.sql", -1)]),
('source --special /', [('/dir1', -1), ('/file1.sql', -1), ('/file2.sql', -1)]),
Expand Down
28 changes: 15 additions & 13 deletions test/pytests/test_special_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@
@pytest.mark.parametrize(
('arg', 'expected'),
[
('query.sql', ('query.sql', False, False, False, 0.0)),
('--special query.sql', ('query.sql', True, False, False, 0.0)),
('--show query.sql', ('query.sql', False, True, False, 0.0)),
('--page query.sql', ('query.sql', False, False, True, 0.0)),
('--special --show --page query file.sql', ('query file.sql', True, True, True, 0.0)),
('--page --show --special query file.sql', ('query file.sql', True, True, True, 0.0)),
('--show --show query.sql', ('query.sql', False, True, False, 0.0)),
('--page --page query.sql', ('query.sql', False, False, True, 0.0)),
('--show', ('', False, True, False, 0.0)),
('--throttle 0.25 query.sql', ('query.sql', False, False, False, 0.25)),
('--throttle=1e-2 query.sql', ('query.sql', False, False, False, 0.01)),
('--throttle 1 --show --throttle=0.5 query.sql', ('query.sql', False, True, False, 0.5)),
('query.sql', ('query.sql', False, False, False, 0.0, False)),
('--special query.sql', ('query.sql', True, False, False, 0.0, False)),
('--show query.sql', ('query.sql', False, True, False, 0.0, False)),
('--page query.sql', ('query.sql', False, False, True, 0.0, False)),
('--special --show --page query file.sql', ('query file.sql', True, True, True, 0.0, False)),
('--page --show --special query file.sql', ('query file.sql', True, True, True, 0.0, False)),
('--show --show query.sql', ('query.sql', False, True, False, 0.0, False)),
('--page --page query.sql', ('query.sql', False, False, True, 0.0, False)),
('--show', ('', False, True, False, 0.0, False)),
('--throttle 0.25 query.sql', ('query.sql', False, False, False, 0.25, False)),
('--throttle=1e-2 query.sql', ('query.sql', False, False, False, 0.01, False)),
('--throttle 1 --show --throttle=0.5 query.sql', ('query.sql', False, True, False, 0.5, False)),
('--help', ('', False, False, False, 0.0, True)),
('--show --help ignored.sql', ('', False, True, False, 0.0, True)),
],
)
def test_parse_source_arguments(arg: str, expected: tuple[str, bool, bool, bool, float]) -> None:
def test_parse_source_arguments(arg: str, expected: tuple[str, bool, bool, bool, float, bool]) -> None:
assert source.parse_source_arguments(arg) == expected


Expand Down