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
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Breaking Changes
* Remove support for `my.cnf` vendor MySQL option files.
* Remove support for `.myclirc` files in the current working directory.
* Make `--batch` and `--execute` non-interactive by default.
* Migrate `default_character_set` out of the `[main]` section of `~/.myclirc`.
* Migrate `ssl_mode` out of the `[main]` section of `~/.myclirc`.


Features
Expand Down
36 changes: 32 additions & 4 deletions mycli/app_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,39 @@
from mycli.client import MyCli


def normalize_ssl_mode(config: ConfigObj) -> tuple[str | None, str | None]:
ssl_mode = config['main'].get('ssl_mode', None) or config['connection'].get('default_ssl_mode', None)
def normalize_ssl_mode(
config: ConfigObj,
config_without_package_defaults: ConfigObj,
) -> tuple[str | None, str | None]:
error_notice: str | None = None
ssl_mode: str | None = None

if 'main' in config_without_package_defaults and 'ssl_mode' in config_without_package_defaults['main']:
# migration with notice added with mycli 2.0.0 in 2026-07
# todo: entirely remove support for ssl_mode in [main]
error_notice = (
'Mycli 2.0 migration: automatically moving ssl_mode under [main] to default_ssl_mode under [connection] in ~/.myclirc .'
)

ssl_mode = config_without_package_defaults['main']['ssl_mode']

config_without_package_defaults.encoding = 'utf-8'
if 'connection' not in config_without_package_defaults:
config_without_package_defaults['connection'] = {}
if config_without_package_defaults['connection'].get('default_ssl_mode', None) in (None, ''):
config_without_package_defaults['connection']['default_ssl_mode'] = ssl_mode
else:
ssl_mode = config_without_package_defaults['connection'].get('default_ssl_mode')
error_notice += f'\nBut connection.default_ssl_mode already existed, with the value: "{ssl_mode}".'
del config_without_package_defaults['main']['ssl_mode']
config_without_package_defaults.write()

if not ssl_mode and 'default_ssl_mode' in config['connection']:
ssl_mode = config['connection']['default_ssl_mode']
if ssl_mode not in ('auto', 'on', 'off', None):
return None, f'Invalid config option provided for ssl_mode ({ssl_mode}); ignoring.'
return ssl_mode, None
error_notice = f'Invalid config option provided for ssl_mode ({ssl_mode}); ignoring.'
return None, error_notice
return ssl_mode, error_notice


def configure_prompt_state(
Expand Down
2 changes: 1 addition & 1 deletion mycli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def __init__(
self.binary_display = c['main'].get('binary_display')
self.llm_prompt_field_truncate, self.llm_prompt_section_truncate = llm_prompt_truncation(c)

self.ssl_mode, ssl_mode_error = normalize_ssl_mode(c)
self.ssl_mode, ssl_mode_error = normalize_ssl_mode(c, self.config_without_package_defaults)
if ssl_mode_error:
self.echo(ssl_mode_error, err=True, fg="red")

Expand Down
32 changes: 26 additions & 6 deletions mycli/client_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,34 @@ def connect(

passwd = passwd if isinstance(passwd, (str, int)) else mylogin_cnf["password"]

# default_character_set doesn't check in self.config_without_package_defaults, because the
# option already existed before the my.cnf deprecation. For the same reason,
# default_character_set can be in [connection] or [main].
if not character_set:
if 'default_character_set' in self.config['connection']:
if 'main' in self.config_without_package_defaults and 'default_character_set' in self.config_without_package_defaults['main']:
# migration with notice added with mycli 2.0.0 in 2026-07
# todo: entirely remove support for default_character_set in [main]
click.secho(
'Mycli 2.0 migration: automatically moving default_character_set from [main] to [connection] in ~/.myclirc .',
err=True,
fg='red',
)
character_set = self.config_without_package_defaults['main']['default_character_set']

self.config_without_package_defaults.encoding = 'utf-8'
if 'connection' not in self.config_without_package_defaults:
self.config_without_package_defaults['connection'] = {}
if self.config_without_package_defaults['connection'].get('default_character_set', None) in (None, ''):
self.config_without_package_defaults['connection']['default_character_set'] = character_set
else:
character_set = self.config_without_package_defaults["connection"].get("default_character_set")
click.secho(
f'But connection.default_character_set already existed, with the value: "{character_set}".',
err=True,
fg='red',
)
del self.config_without_package_defaults['main']['default_character_set']
self.config_without_package_defaults.write()

if not character_set and 'default_character_set' in self.config['connection']:
character_set = self.config['connection']['default_character_set']
elif 'default_character_set' in self.config['main']:
character_set = self.config['main']['default_character_set']
if not character_set:
character_set = DEFAULT_CHARSET

Expand Down
49 changes: 44 additions & 5 deletions test/pytests/test_app_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,60 @@ def __init__(self, login_path: str | None = None) -> None:

@pytest.mark.parametrize('ssl_mode', ['auto', 'on', 'off'])
def test_normalize_ssl_mode_accepts_known_values(ssl_mode: str) -> None:
config = ConfigObj({'main': {'ssl_mode': ssl_mode}, 'connection': {'default_ssl_mode': 'off'}})
config = ConfigObj({'main': {'ssl_mode': ssl_mode}, 'connection': {'default_ssl_mode': ssl_mode}})
config_wo = ConfigObj({'main': {}, 'connection': {}})

assert normalize_ssl_mode(config) == (ssl_mode, None)
assert normalize_ssl_mode(config, config_wo) == (ssl_mode, None)


def test_normalize_ssl_mode_falls_back_to_connection_default() -> None:
config = ConfigObj({'main': {'ssl_mode': ''}, 'connection': {'default_ssl_mode': 'on'}})
config_wo = ConfigObj({'main': {}, 'connection': {}})

assert normalize_ssl_mode(config) == ('on', None)
assert normalize_ssl_mode(config, config_wo) == ('on', None)


def test_normalize_ssl_mode_returns_none_when_not_configured() -> None:
config = ConfigObj({'main': {}, 'connection': {}})
config_wo = ConfigObj({'main': {}, 'connection': {}})

assert normalize_ssl_mode(config, config_wo) == (None, None)


def test_normalize_ssl_mode_migrates_deprecated_main_value() -> None:
config = ConfigObj({'main': {}, 'connection': {'default_ssl_mode': 'off'}})
config_wo = ConfigObj({'main': {'ssl_mode': 'on'}})

ssl_mode, warning = normalize_ssl_mode(config, config_wo)

assert ssl_mode == 'on'
assert (
warning == 'Mycli 2.0 migration: automatically moving ssl_mode under [main] to default_ssl_mode under [connection] in ~/.myclirc .'
)
assert config_wo['connection']['default_ssl_mode'] == 'on'
assert 'ssl_mode' not in config_wo['main']


def test_normalize_ssl_mode_uses_existing_connection_value_when_migrating() -> None:
config = ConfigObj({'main': {}, 'connection': {'default_ssl_mode': 'off'}})
config_wo = ConfigObj({'main': {'ssl_mode': 'on'}, 'connection': {'default_ssl_mode': 'off'}})

ssl_mode, warning = normalize_ssl_mode(config, config_wo)

assert ssl_mode == 'off'
assert warning == (
'Mycli 2.0 migration: automatically moving ssl_mode under [main] to default_ssl_mode under [connection] in ~/.myclirc .'
'\nBut connection.default_ssl_mode already existed, with the value: "off".'
)
assert config_wo['connection']['default_ssl_mode'] == 'off'
assert 'ssl_mode' not in config_wo['main']


def test_normalize_ssl_mode_reports_invalid_values() -> None:
config = ConfigObj({'main': {'ssl_mode': 'required'}, 'connection': {'default_ssl_mode': 'off'}})
config = ConfigObj({'main': {'ssl_mode': 'required'}, 'connection': {'default_ssl_mode': 'required'}})
config_wo = ConfigObj()

ssl_mode, warning = normalize_ssl_mode(config)
ssl_mode, warning = normalize_ssl_mode(config, config_wo)

assert ssl_mode is None
assert warning == 'Invalid config option provided for ssl_mode (required); ignoring.'
Expand Down
29 changes: 24 additions & 5 deletions test/pytests/test_checkup.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ def test_configuration_checkup_reports_missing_unsupported_and_deprecated(capsys
'main': {
'present': '',
'unsupported_item': '',
'default_character_set': '',
},
'unsupported_section': {
'anything': '',
Expand Down Expand Up @@ -162,9 +161,6 @@ def test_configuration_checkup_reports_missing_unsupported_and_deprecated(capsys
assert '### Unsupported in user ~/.myclirc:' in output
assert 'The entire section:\n\n [unsupported_section]\n' in output
assert 'The item:\n\n [main]\n unsupported_item =' in output
assert '### Deprecated in user ~/.myclirc:' in output
assert ' [main]\n default_character_set' in output
assert ' [connection]\n default_character_set' in output
assert f'{checkup.REPO_URL}/blob/main/mycli/myclirc' in output


Expand Down Expand Up @@ -200,10 +196,33 @@ def test_configuration_checkup_skips_transitioned_and_free_entry_items(capsys) -
assert 'The entire section:\n\n [extra_section]\n' in output
assert 'Unsupported in user ~/.myclirc:' in output
assert 'The entire section:\n\n [unsupported_section]\n' in output
assert '[connection]\n default_character_set =' not in output
assert '[favorite_queries]' not in output


def test_configuration_checkup_reports_deprecated_transition(capsys) -> None:
mycli = SimpleNamespace(
config={
'main': {},
},
config_without_package_defaults={
'main': {
'ssl_mode': '',
},
},
config_without_user_options={
'main': {},
},
)

checkup._configuration_checkup(mycli)
output = capsys.readouterr().out

assert '### Deprecated in user ~/.myclirc:' in output
assert 'It is recommended to transition:\n\n [main]\n ssl_mode\n\nto\n\n [connection]\n default_ssl_mode' in output
assert '### Unsupported in user ~/.myclirc:' not in output
assert f'{checkup.REPO_URL}/blob/main/mycli/myclirc' in output


def test_configuration_checkup_up_to_date(capsys) -> None:
mycli = SimpleNamespace(
config={
Expand Down
4 changes: 2 additions & 2 deletions test/pytests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def test_init_reports_invalid_ssl_mode(monkeypatch: pytest.MonkeyPatch, tmp_path
myclirc = write_myclirc(
tmp_path,
"""
[main]
ssl_mode = invalid
[connection]
default_ssl_mode = invalid
""",
)

Expand Down
73 changes: 71 additions & 2 deletions test/pytests/test_client_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ def echo(self, *args: Any, **kwargs: Any) -> None:
self.echo_calls.append((args, kwargs))


class WritableConfig(dict[str, Any]):
encoding: str | None = None

def __init__(self, value: dict[str, Any]) -> None:
super().__init__(value)
self.write_calls = 0

def write(self) -> None:
self.write_calls += 1


class FakeSQLExecute:
calls: list[dict[str, Any]] = []
effects: list[Any] = []
Expand Down Expand Up @@ -148,12 +159,70 @@ def test_connect_uses_character_set_from_connection_config() -> None:
assert FakeSQLExecute.calls[-1]['character_set'] == 'utf16'


def test_connect_uses_character_set_from_main_config() -> None:
client = DummyClient(config={'main': {'default_character_set': 'utf32'}, 'connection': {}})
def test_connect_migrates_deprecated_character_set_from_main_config(
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_wo = WritableConfig({'main': {'default_character_set': 'utf32'}})
client = DummyClient(
config={'main': {}, 'connection': {'default_character_set': 'utf16'}},
config_without_package_defaults=config_wo,
)
secho_calls: list[tuple[str, dict[str, Any]]] = []
monkeypatch.setattr(
client_connection.click,
'secho',
lambda message, **kwargs: secho_calls.append((message, kwargs)),
)

client.connect(host='db', port=3307)

assert FakeSQLExecute.calls[-1]['character_set'] == 'utf32'
assert config_wo.encoding == 'utf-8'
assert config_wo['connection']['default_character_set'] == 'utf32'
assert 'default_character_set' not in config_wo['main']
assert config_wo.write_calls == 1
assert secho_calls == [
(
'Mycli 2.0 migration: automatically moving default_character_set from [main] to [connection] in ~/.myclirc .',
{'err': True, 'fg': 'red'},
)
]


def test_connect_uses_existing_connection_character_set_when_migrating(
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_wo = WritableConfig({
'main': {'default_character_set': 'utf32'},
'connection': {'default_character_set': 'utf16'},
})
client = DummyClient(
config={'main': {}, 'connection': {'default_character_set': 'latin1'}},
config_without_package_defaults=config_wo,
)
secho_calls: list[tuple[str, dict[str, Any]]] = []
monkeypatch.setattr(
client_connection.click,
'secho',
lambda message, **kwargs: secho_calls.append((message, kwargs)),
)

client.connect(host='db', port=3307)

assert FakeSQLExecute.calls[-1]['character_set'] == 'utf16'
assert config_wo['connection']['default_character_set'] == 'utf16'
assert 'default_character_set' not in config_wo['main']
assert config_wo.write_calls == 1
assert secho_calls == [
(
'Mycli 2.0 migration: automatically moving default_character_set from [main] to [connection] in ~/.myclirc .',
{'err': True, 'fg': 'red'},
),
(
'But connection.default_character_set already existed, with the value: "utf16".',
{'err': True, 'fg': 'red'},
),
]


def test_connect_uses_default_character_set_when_none_configured() -> None:
Expand Down
Loading