Skip to content
Open
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 @@ -4,6 +4,7 @@ Upcoming (TBD)
Features
---------
* Remove undocumented `%mycli` Jupyter magic.
* Add `--quiet` option, and let `--verbose` be given multiple times.


Bug Fixes
Expand Down
26 changes: 22 additions & 4 deletions mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def __init__(
warn: bool | None = None,
myclirc: str = "~/.myclirc",
show_warnings: bool | None = None,
cli_verbosity: int = 0,
) -> None:
self.sqlexecute = sqlexecute
self.logfile = logfile
Expand Down Expand Up @@ -194,7 +195,9 @@ def __init__(
self.main_formatter.mycli = self
self.redirect_formatter.mycli = self
self.syntax_style = c["main"]["syntax_style"]
self.less_chatty = c["main"].as_bool("less_chatty")
self.verbosity = -1 if c["main"].as_bool("less_chatty") else 0
if cli_verbosity:
self.verbosity = cli_verbosity
self.cli_style = c["colors"]
self.ptoolkit_style = style_factory_ptoolkit(self.syntax_style, self.cli_style)
self.helpers_style = style_factory_helpers(self.syntax_style, self.cli_style)
Expand Down Expand Up @@ -1306,10 +1309,15 @@ class CliArgs:
is_flag=True,
help=("""Verify server's "Common Name" in its cert against hostname used when connecting. This option is disabled by default."""),
)
verbose: bool = clickdc.option(
verbose: int = clickdc.option(
'-v',
count=True,
help='More verbose output and feedback. Can be given multiple times.',
)
quiet: bool = clickdc.option(
'-q',
is_flag=True,
help='Verbose output.',
help='Less verbose output and feedback.',
)
dbname: str | None = clickdc.option(
'-D',
Expand Down Expand Up @@ -1514,6 +1522,15 @@ def get_password_from_file(password_file: str | None) -> str | None:
if cli_args.password is None and os.environ.get("MYSQL_PWD") is not None:
cli_args.password = os.environ.get("MYSQL_PWD")

cli_verbosity = 0
if cli_args.verbose and cli_args.quiet:
click.secho('Error: --verbose and --quiet are incompatible.', err=True, fg='red')
sys.exit(1)
elif cli_args.verbose:
cli_verbosity = int(cli_args.verbose)
elif cli_args.quiet:
cli_verbosity = -1

mycli = MyCli(
prompt=cli_args.prompt,
toolbar_format=cli_args.toolbar,
Expand All @@ -1525,6 +1542,7 @@ def get_password_from_file(password_file: str | None) -> str | None:
warn=cli_args.warn,
myclirc=cli_args.myclirc,
show_warnings=cli_args.show_warnings,
cli_verbosity=cli_verbosity,
)

if cli_args.checkup:
Expand Down Expand Up @@ -1576,7 +1594,7 @@ def get_password_from_file(password_file: str | None) -> str | None:
)

if cli_args.list_dsn:
sys.exit(main_list_dsn(mycli, cli_args))
sys.exit(main_list_dsn(mycli))

if cli_args.list_ssh_config:
sys.exit(main_list_ssh_config(mycli, cli_args))
Expand Down
6 changes: 3 additions & 3 deletions mycli/main_modes/list_dsn.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import click

if TYPE_CHECKING:
from mycli.main import CliArgs, MyCli
from mycli.main import MyCli


def main_list_dsn(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
def main_list_dsn(mycli: 'MyCli') -> int:
try:
alias_dsn = mycli.config['alias_dsn']
except KeyError:
Expand All @@ -18,7 +18,7 @@ def main_list_dsn(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
click.secho(str(e), err=True, fg='red')
return 1
for alias, value in alias_dsn.items():
if cli_args.verbose:
if mycli.verbosity >= 1:
click.secho(f'{alias} : {value}')
else:
click.secho(alias)
Expand Down
2 changes: 1 addition & 1 deletion mycli/main_modes/list_ssh_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def main_list_ssh_config(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
click.secho('Error reading ssh config', err=True, fg="red")
return 1
for host_entry in host_entries:
if cli_args.verbose:
if mycli.verbosity >= 1:
host_config = ssh_config.lookup(host_entry)
click.secho(f"{host_entry} : {host_config.get('hostname')}")
else:
Expand Down
4 changes: 2 additions & 2 deletions mycli/main_modes/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def _show_startup_banner(
mycli: 'MyCli',
sqlexecute: SQLExecute,
) -> None:
if mycli.less_chatty:
if mycli.verbosity < 0:
return

if sqlexecute.server_info is not None:
Expand Down Expand Up @@ -807,5 +807,5 @@ def main_repl(mycli: 'MyCli') -> None:
state.iterations += 1
except EOFError:
special.close_tee()
if not mycli.less_chatty:
if mycli.verbosity >= 0:
mycli.echo('Goodbye!')
3 changes: 2 additions & 1 deletion mycli/myclirc
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ multiplex_window_title = ''
# as frequently as the database is changed.
multiplex_pane_title = ''

# Skip intro info on startup and outro info on exit
# Skip intro info on startup and outro info on exit, and generally reduce
# feedback. This is equivalent to giving --quiet at the command line.
less_chatty = False

# Use alias from --login-path instead of host name in prompt
Expand Down
6 changes: 3 additions & 3 deletions mycli/packages/special/dbcommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def list_tables(
cur: Cursor,
arg: str | None = None,
_arg_type: ArgType = ArgType.PARSED_QUERY,
verbose: bool = False,
command_verbosity: bool = False,
) -> list[SQLResult]:
if arg:
query = f'SHOW FIELDS FROM {arg}'
Expand All @@ -33,10 +33,10 @@ def list_tables(
return [SQLResult()]

# Fetch results before potentially executing another query
results = list(cur.fetchall()) if verbose and arg else cur
results = list(cur.fetchall()) if command_verbosity and arg else cur

postamble = ''
if verbose and arg:
if command_verbosity and arg:
query = f'SHOW CREATE TABLE {arg}'
logger.debug(query)
cur.execute(query)
Expand Down
8 changes: 4 additions & 4 deletions mycli/packages/special/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
LLM_CLI_IMPORTED = False
from pymysql.cursors import Cursor

from mycli.packages.special.main import Verbosity, parse_special_command
from mycli.packages.special.main import CommandVerbosity, parse_special_command
from mycli.packages.sqlresult import SQLResult

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -224,7 +224,7 @@ def handle_llm(
prompt_field_truncate: int,
prompt_section_truncate: int,
) -> tuple[str, str | None, float]:
_, verbosity, arg = parse_special_command(text)
_, command_verbosity, arg = parse_special_command(text)
if not LLM_IMPORTED:
raise FinishIteration(results=[SQLResult(preamble=NEED_DEPENDENCIES)])
if arg.strip().lower() in ['', 'help', '?', r'\?']:
Expand Down Expand Up @@ -262,7 +262,7 @@ def handle_llm(
sql = match.group(1).strip()
else:
raise FinishIteration(results=[SQLResult(preamble=output)])
return (output if verbosity == Verbosity.SUCCINCT else "", sql, end - start)
return (output if command_verbosity == CommandVerbosity.SUCCINCT else "", sql, end - start)
else:
run_external_cmd("llm", *args, restart_cli=restart)
raise FinishIteration(results=None)
Expand All @@ -277,7 +277,7 @@ def handle_llm(
prompt_section_truncate=prompt_section_truncate,
)
end = time()
if verbosity == Verbosity.SUCCINCT:
if command_verbosity == CommandVerbosity.SUCCINCT:
context = ""
return (context, sql, end - start)
except Exception as e:
Expand Down
16 changes: 8 additions & 8 deletions mycli/packages/special/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,21 @@ class CommandNotFound(Exception):
pass


class Verbosity(Enum):
class CommandVerbosity(Enum):
SUCCINCT = "succinct"
NORMAL = "normal"
VERBOSE = "verbose"


def parse_special_command(sql: str) -> tuple[str, Verbosity, str]:
def parse_special_command(sql: str) -> tuple[str, CommandVerbosity, str]:
command, _, arg = sql.partition(" ")
verbosity = Verbosity.NORMAL
command_verbosity = CommandVerbosity.NORMAL
if "+" in command:
verbosity = Verbosity.VERBOSE
command_verbosity = CommandVerbosity.VERBOSE
elif "-" in command:
verbosity = Verbosity.SUCCINCT
command_verbosity = CommandVerbosity.SUCCINCT
command = command.strip().strip("+-")
return (command, verbosity, arg.strip())
return (command, command_verbosity, arg.strip())


def special_command(
Expand Down Expand Up @@ -130,7 +130,7 @@ def execute(cur: Cursor, sql: str) -> list[SQLResult]:
"""Execute a special command and return the results. If the special command
is not supported a CommandNotFound will be raised.
"""
command, verbosity, arg = parse_special_command(sql)
command, command_verbosity, arg = parse_special_command(sql)

if (command not in COMMANDS) and (command.lower() not in COMMANDS):
raise CommandNotFound(f'Command not found: {command}')
Expand All @@ -150,7 +150,7 @@ def execute(cur: Cursor, sql: str) -> list[SQLResult]:
if special_cmd.arg_type == ArgType.NO_QUERY:
return special_cmd.handler()
elif special_cmd.arg_type == ArgType.PARSED_QUERY:
return special_cmd.handler(cur=cur, arg=arg, verbose=(verbosity == Verbosity.VERBOSE))
return special_cmd.handler(cur=cur, arg=arg, command_verbosity=(command_verbosity == CommandVerbosity.VERBOSE))
elif special_cmd.arg_type == ArgType.RAW_QUERY:
return special_cmd.handler(cur=cur, query=sql)

Expand Down
3 changes: 2 additions & 1 deletion test/myclirc
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ multiplex_window_title = ''
# as frequently as the database is changed.
multiplex_pane_title = ''

# Skip intro info on startup and outro info on exit
# Skip intro info on startup and outro info on exit, and generally reduce
# feedback. This is equivalent to giving --quiet at the command line.
less_chatty = True

# Use alias from --login-path instead of host name in prompt
Expand Down
30 changes: 30 additions & 0 deletions test/pytests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2126,6 +2126,36 @@ def test_execute_arg_warns_about_ignoring_stdin(monkeypatch):
assert 'Ignoring STDIN' in result.output


def test_verbose_and_quiet_are_incompatible() -> None:
runner = CliRunner()

result = runner.invoke(click_entrypoint, args=['--verbose', '--quiet'])

assert result.exit_code == 1
assert 'incompatible.' in result.output


def test_quiet_sets_negative_cli_verbosity(monkeypatch: pytest.MonkeyPatch) -> None:
dummy_class = make_dummy_mycli_class(
config={
'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'true'},
'connection': {'default_keepalive_ticks': 0},
'alias_dsn': {},
}
)
monkeypatch.setattr(main, 'MyCli', dummy_class)
monkeypatch.setattr(main.sys, 'stdin', SimpleNamespace(isatty=lambda: True))

cli_args = main.CliArgs()
cli_args.quiet = True

call_click_entrypoint_direct(cli_args)

dummy = dummy_class.last_instance
assert dummy is not None
assert dummy.init_kwargs['cli_verbosity'] == -1


def test_execute_arg_supersedes_batch_file(monkeypatch):
mycli_main, mycli_main_batch, MockMyCli = noninteractive_mock_mycli(monkeypatch)
runner = CliRunner()
Expand Down
16 changes: 9 additions & 7 deletions test/pytests/test_main_modes_list_dsn.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@dataclass
class DummyCliArgs:
verbose: bool = False
verbose: int = 0


class DummyConfig:
Expand All @@ -25,10 +25,11 @@ def __getitem__(self, key: str) -> dict[str, str]:
class DummyMyCli:
def __init__(self, config: Any) -> None:
self.config = config
self.verbosity = 0


def main_list_dsn(mycli: DummyMyCli, cli_args: DummyCliArgs) -> int:
return list_dsn_mode.main_list_dsn(cast(Any, mycli), cast(Any, cli_args))
def main_list_dsn(mycli: DummyMyCli) -> int:
return list_dsn_mode.main_list_dsn(cast(Any, mycli))


def test_main_list_dsn_lists_aliases_without_values(monkeypatch) -> None:
Expand All @@ -41,7 +42,7 @@ def test_main_list_dsn_lists_aliases_without_values(monkeypatch) -> None:
lambda message, err=None, fg=None: secho_calls.append((message, err, fg)),
)

result = main_list_dsn(mycli, DummyCliArgs(verbose=False))
result = main_list_dsn(mycli)

assert result == 0
assert secho_calls == [
Expand All @@ -53,14 +54,15 @@ def test_main_list_dsn_lists_aliases_without_values(monkeypatch) -> None:
def test_main_list_dsn_lists_aliases_with_values_in_verbose_mode(monkeypatch) -> None:
secho_calls: list[tuple[str, bool | None, str | None]] = []
mycli = DummyMyCli(DummyConfig({'prod': 'mysql://u:p@h/db'}))
mycli.verbosity = 1

monkeypatch.setattr(
list_dsn_mode.click,
'secho',
lambda message, err=None, fg=None: secho_calls.append((message, err, fg)),
)

result = main_list_dsn(mycli, DummyCliArgs(verbose=True))
result = main_list_dsn(mycli)

assert result == 0
assert secho_calls == [('prod : mysql://u:p@h/db', None, None)]
Expand All @@ -76,7 +78,7 @@ def test_main_list_dsn_reports_invalid_alias_section(monkeypatch) -> None:
lambda message, err=None, fg=None: secho_calls.append((message, err, fg)),
)

result = main_list_dsn(mycli, DummyCliArgs())
result = main_list_dsn(mycli)

assert result == 1
assert secho_calls == [
Expand All @@ -98,7 +100,7 @@ def test_main_list_dsn_reports_other_config_errors(monkeypatch) -> None:
lambda message, err=None, fg=None: secho_calls.append((message, err, fg)),
)

result = main_list_dsn(mycli, DummyCliArgs())
result = main_list_dsn(mycli)

assert result == 1
assert secho_calls == [('boom', True, 'red')]
16 changes: 12 additions & 4 deletions test/pytests/test_main_modes_list_ssh_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
@dataclass
class DummyCliArgs:
ssh_config_path: str = 'ssh_config'
verbose: bool = False
verbose: int = 0


class DummyMyCli:
def __init__(self, config: Any) -> None:
self.config = config
self.verbosity = 0


class DummySSHConfig:
Expand All @@ -27,7 +33,9 @@ def lookup(self, hostname: str) -> dict[str, str]:


def main_list_ssh_config(cli_args: DummyCliArgs) -> int:
return list_ssh_config_mode.main_list_ssh_config(cast(Any, object()), cast(Any, cli_args))
mycli = DummyMyCli(config={})
mycli.verbosity = cli_args.verbose
return list_ssh_config_mode.main_list_ssh_config(cast(Any, mycli), cast(Any, cli_args))


def test_main_list_ssh_config_lists_hostnames(monkeypatch) -> None:
Expand All @@ -41,7 +49,7 @@ def test_main_list_ssh_config_lists_hostnames(monkeypatch) -> None:
lambda message, err=None, fg=None: secho_calls.append((message, err, fg)),
)

result = main_list_ssh_config(DummyCliArgs(verbose=False))
result = main_list_ssh_config(DummyCliArgs(verbose=0))

assert result == 0
assert secho_calls == [
Expand All @@ -64,7 +72,7 @@ def test_main_list_ssh_config_lists_verbose_host_details(monkeypatch) -> None:
lambda message, err=None, fg=None: secho_calls.append((message, err, fg)),
)

result = main_list_ssh_config(DummyCliArgs(verbose=True))
result = main_list_ssh_config(DummyCliArgs(verbose=1))

assert result == 0
assert secho_calls == [('prod : db.example.com', None, None)]
Expand Down
Loading