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 @@ -9,6 +9,7 @@ Features
Bugfixes
---------
* Don't allow saving favorite queries to rewrite comments in `~/.myclirc`.
* Don't allow saving favorite queries to rewrite quoting in `~/.myclirc`.
* Don't allow saving named DSNs to rewrite comments in `~/.myclirc`.


Expand Down
24 changes: 22 additions & 2 deletions mycli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@
logger = logging.getLogger(__name__)


class LimiitedQuotePreservingConfigObj(ConfigObj):
"""Useful for saving individual items without modifying the whole file."""

def __init__(self, *args, **kwargs):
ConfigObj.__init__(self, *args, **kwargs)

def _unquote(self, value):
return value

def _quote(self, value, multiline=True):
return value


def log(logger: logging.Logger, level: int, message: str) -> None:
"""Logs message to stderr if logging isn't initialized."""

Expand All @@ -23,7 +36,11 @@ def log(logger: logging.Logger, level: int, message: str) -> None:
logger.log(level, message)


def read_config_file(f: str | IO[str], list_values: bool = True) -> ConfigObj | None:
def read_config_file(
f: str | IO[str],
list_values: bool = True,
preserve_quotes: bool = False,
) -> ConfigObj | LimiitedQuotePreservingConfigObj | None:
"""Read a config file.

*list_values* set to `True` is the default behavior of ConfigObj.
Expand All @@ -38,7 +55,10 @@ def read_config_file(f: str | IO[str], list_values: bool = True) -> ConfigObj |
f = os.path.expanduser(f)

try:
config = ConfigObj(f, interpolation=False, encoding="utf8", list_values=list_values)
if preserve_quotes:
config = LimiitedQuotePreservingConfigObj(f, interpolation=False, encoding="utf8", list_values=False)
else:
config = ConfigObj(f, interpolation=False, encoding="utf8", list_values=list_values)
except ConfigObjError as e:
log(logger, logging.WARNING, "Unable to parse line {0} of config file '{1}'.".format(e.line_number, f))
log(logger, logging.WARNING, "Using successfully parsed config values.")
Expand Down
10 changes: 8 additions & 2 deletions mycli/packages/special/favoritequeries.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def _config_for_write(self) -> Any:
if self.config_file is None:
return self.config

config = read_config_file(self.config_file)
config = read_config_file(self.config_file, preserve_quotes=True)
if config is None:
raise OSError(f"Unable to read config file '{os.path.expanduser(self.config_file)}'.")
return config
Expand All @@ -144,10 +144,15 @@ def get(self, name) -> str | None:

def save(self, name: str, query: str) -> None:
config = self._config_for_write()
query = query.rstrip(' \t\n\r;')
if '\n' in query:
manually_quoted_query = f"'''{query}'''"
else:
manually_quoted_query = query
config.encoding = "utf-8"
section_existed = self.section_name in config
previous_query = config.get(self.section_name, {}).get(name, MISSING)
self._set_query(config, name, query)
self._set_query(config, name, manually_quoted_query)
try:
config.write()
except Exception:
Expand All @@ -160,6 +165,7 @@ def save(self, name: str, query: str) -> None:
raise

if config is not self.config:
# use the unquoted query for the current session
self._set_query(self.config, name, query)

def delete(self, name: str) -> str:
Expand Down
24 changes: 23 additions & 1 deletion test/pytests/test_favoritequeries.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@ def test_save_updates_existing_section_and_writes_config() -> None:
assert config.write_calls == 1


def test_save_quotes_multiline_query_for_disk_and_keeps_runtime_query_unquoted(
monkeypatch: pytest.MonkeyPatch,
) -> None:
write_config = DummyConfig()
runtime_config = DummyConfig()
favorites = FavoriteQueries(runtime_config, '/tmp/myclirc')
read_calls: list[tuple[str, bool]] = []

def read_config_file(path: str, preserve_quotes: bool = False) -> DummyConfig:
read_calls.append((path, preserve_quotes))
return write_config

monkeypatch.setattr(favoritequeries_module, 'read_config_file', read_config_file)

favorites.save('report', 'select 1;\nselect 2;\n')

assert read_calls == [('/tmp/myclirc', True)]
assert write_config['favorite_queries']['report'] == "'''select 1;\nselect 2'''"
assert write_config.write_calls == 1
assert runtime_config['favorite_queries']['report'] == 'select 1;\nselect 2'


def test_delete_removes_existing_favorite_and_writes_config() -> None:
config = DummyConfig({'favorite_queries': {'demo': 'select 1'}})
favorites = FavoriteQueries(config)
Expand Down Expand Up @@ -222,7 +244,7 @@ def test_save_does_not_update_runtime_config_when_user_config_cannot_be_read(
) -> None:
merged_config = DummyConfig({'favorite_queries': {'existing': 'select 1'}})
favorites = FavoriteQueries(merged_config, '~/.myclirc')
monkeypatch.setattr(favoritequeries_module, 'read_config_file', lambda _path: None)
monkeypatch.setattr(favoritequeries_module, 'read_config_file', lambda _path, **_kwargs: None)

with pytest.raises(OSError, match=r"Unable to read config file '.*/\.myclirc'\."):
favorites.save('new', 'select 2')
Expand Down
Loading