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 @@ -5,6 +5,7 @@ Bug Fixes
--------
* Let the `beep_after_seconds` slow query alert happen before paged output.
* Allow comma-containing favorite queries in `~/.myclirc`.
* Allow comma-containing init-commands in `~/.myclirc`.


Internal
Expand Down
5 changes: 5 additions & 0 deletions doc/known_bugs.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ The prompt and toolbar are updated more often than needed, even with caching.

When syntax highlighting with `pygmentize` is enabled for history search previews,
the colors do not match the user's preferences in `~/.myclirc`.

## favorite queries

Favorite queries are triple-quoted on save in `~/.myclirc`. If the text of the SQL
in the favorite query contains both possible triple-quote delimiters, this fails.
12 changes: 7 additions & 5 deletions mycli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
logger = logging.getLogger(__name__)


class FavoriteQueryPreservingConfigObj(ConfigObj):
class SQLValuePreservingConfigObj(ConfigObj):
"""When reading, quote SQL text on the fly which ConfigObj would otherwise interpret as a list."""

# Buglet: the use of _get_triple_quote() does not allow values which
Expand All @@ -25,7 +25,7 @@ def _parse(self, infile: list[str]) -> None:
return

lines = infile.copy()
in_favorites = False
in_sql_stanza = False
index = 0
while index < len(lines):
line = lines[index]
Expand All @@ -35,7 +35,9 @@ def _parse(self, infile: list[str]) -> None:
section = self._sectionmarker.match(line)
if section is not None:
_, opening, name, closing, _ = section.groups()
in_favorites = opening.count('[') == closing.count(']') == 1 and self._unquote(name) == 'favorite_queries'
in_sql_stanza = opening.count('[') == closing.count(']') == 1 and (
self._unquote(name) == 'favorite_queries' or 'init-commands' in name
)
else:
entry = self._keyword.match(line)
if entry is not None:
Expand All @@ -46,7 +48,7 @@ def _parse(self, infile: list[str]) -> None:
_, _, index = self._multiline(value, lines, index, len(lines) - 1)
except SyntaxError:
break
elif in_favorites and not value.startswith(('"', "'")):
elif in_sql_stanza and not value.startswith(('"', "'")):
match = self._nolistvalue.match(value)
if match is not None:
sql, comment = match.groups()
Expand Down Expand Up @@ -130,7 +132,7 @@ def read_config_file(
if preserve_quotes:
config = LimiitedQuotePreservingConfigObj(f, interpolation=False, encoding="utf8", list_values=False)
else:
config = FavoriteQueryPreservingConfigObj(f, interpolation=False, encoding="utf8", list_values=list_values)
config = SQLValuePreservingConfigObj(f, interpolation=False, encoding="utf8", list_values=list_values)
except ConfigObjError as e:
if raise_errors:
raise
Expand Down
76 changes: 74 additions & 2 deletions mycli_test/pytests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,78 @@ def test_read_config_file_list_values_off():
assert config["main"]["weather"] == "'cloudy with a chance of meatballs'"


@pytest.mark.parametrize('section', ['init-commands', 'alias_dsn.init-commands'])
@pytest.mark.parametrize(
'query',
[
'SET @first = 1, @second = 2',
"SET @message = CONCAT('hello', ', world')",
'''SET @first = 'a,b', @second = "c,d"''',
],
)
def test_init_commands_preserve_unquoted_sql(section: str, query: str) -> None:
config = read_config_file(StringIO(f'[{section}]\nstartup = {query}\n'), raise_errors=True)

assert config is not None
assert config[section]['startup'] == query


@pytest.mark.parametrize('section', ['init-commands', 'alias_dsn.init-commands'])
@pytest.mark.parametrize('quote', ['"', "'", '"""', "'''"])
def test_init_commands_preserve_config_quote_semantics(section: str, quote: str) -> None:
query = 'SET @first = 1, @second = 2'
config = read_config_file(StringIO(f'[{section}]\nstartup = {quote}{query}{quote}\n'), raise_errors=True)

assert config is not None
assert config[section]['startup'] == query


@pytest.mark.parametrize('section', ['init-commands', 'alias_dsn.init-commands'])
def test_init_commands_preserve_multiline_sql(section: str) -> None:
query = "SET @first = 1,\n @second = 'a,b'"
config = read_config_file(StringIO(f'[{section}]\nstartup = """{query}"""\n'), raise_errors=True)

assert config is not None
assert config[section]['startup'] == query


@pytest.mark.parametrize('section', ['init-commands', 'alias_dsn.init-commands'])
def test_init_commands_strip_trailing_config_comment(section: str) -> None:
query = 'SET @first = 1, @second = 2'
config = read_config_file(StringIO(f'[{section}]\nstartup = {query} # Startup settings.\n'), raise_errors=True)

assert config is not None
assert config[section]['startup'] == query


@pytest.mark.parametrize('section', ['init-commands', 'alias_dsn.init-commands'])
def test_init_commands_accept_spaced_quoted_section_name(section: str) -> None:
query = 'SET @first = 1, @second = 2'
config = read_config_file(StringIO(f'[ "{section}" ]\nstartup = {query}\n'), raise_errors=True)

assert config is not None
assert config[section]['startup'] == query


@pytest.mark.parametrize('section', ['init-commands', 'alias_dsn.init-commands'])
def test_init_commands_do_not_change_following_section_lists(section: str) -> None:
config = read_config_file(
StringIO(f'[{section}]\nstartup = SET @first = 1, @second = 2\n[main]\nitems = a, b\n'),
raise_errors=True,
)

assert config is not None
assert config['main']['items'] == ['a', 'b']


@pytest.mark.parametrize('section', ['init-commands', 'alias_dsn.init-commands'])
def test_nested_init_command_section_keeps_list_parsing(section: str) -> None:
config = read_config_file(StringIO(f'[main]\n[[{section}]]\nitems = a, b\n'), raise_errors=True)

assert config is not None
assert config['main'][section]['items'] == ['a', 'b']


def test_quote_preserving_config_retains_quotes_and_quotes_multiline_values() -> None:
config = read_config_file(StringIO('[main]\nquoted = "value"\n'), preserve_quotes=True)

Expand Down Expand Up @@ -294,7 +366,7 @@ def test_read_config_file_permission_error(monkeypatch, caplog) -> None:
def raise_oserror(*_args, **_kwargs):
raise OSError(13, 'denied', '/tmp/test.cnf')

monkeypatch.setattr(config_module, 'FavoriteQueryPreservingConfigObj', raise_oserror)
monkeypatch.setattr(config_module, 'SQLValuePreservingConfigObj', raise_oserror)

with caplog.at_level(logging.WARNING, logger='mycli.config'):
assert read_config_file('/tmp/test.cnf') is None
Expand Down Expand Up @@ -338,7 +410,7 @@ def test_read_config_file_can_raise_io_errors(monkeypatch) -> None:
def raise_oserror(*_args, **_kwargs):
raise error

monkeypatch.setattr(config_module, 'FavoriteQueryPreservingConfigObj', raise_oserror)
monkeypatch.setattr(config_module, 'SQLValuePreservingConfigObj', raise_oserror)

with pytest.raises(OSError) as exc_info:
read_config_file('/tmp/test.cnf', raise_errors=True)
Expand Down
Loading