Skip to content

Commit a5a14b3

Browse files
committed
migrate connection settings from myclirc [main]
* migrate main.default_character_set to the [connection] section if present * migrate main.ssl_mode to connection.default_ssl_mode if present These migrations will affect a modest number of users who installed mycli for the first time during the periods in which the given connection-level settings were in [main] in the default myclirc file. For default_ssl_mode, that is fresh installs between 2026-01-02 and 2026-02-09. For default_character_set, that is fresh installs between 2026-01-22 and 2026-02-02. Users who ran "mycli --checkup" since then were already advised to migrate the settings to the [connection] section. The --checkup instructions for migrating the settings are not removed, though they are somewhat duplicative, to keep the checkup instructions more coherent. Writes to the config file should probably be used sparingly, since some comments and formatting in the user's file can be lost, but this already happens if a favorite query is saved. Motivation: general simplification; bundling all breaking changes into the 2.0 release. The logic for determining whether the user or package configuration controls is too subtle when there are multiple possible locations for the setting. Preparation for release 2.0
1 parent be011eb commit a5a14b3

8 files changed

Lines changed: 202 additions & 25 deletions

File tree

changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Breaking Changes
1010
* Remove support for deprecated SSH jump functionality.
1111
* Remove support for `my.cnf` vendor MySQL option files.
1212
* Remove support for `.myclirc` files in the current working directory.
13+
* Deprecate `default_character_set` in `[main]` section of `~/.myclirc`.
14+
* Deprecate `ssl_mode` in `[main]` section of `~/.myclirc`.
1315

1416

1517
Documentation

mycli/app_state.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,39 @@
1111
from mycli.client import MyCli
1212

1313

14-
def normalize_ssl_mode(config: ConfigObj) -> tuple[str | None, str | None]:
15-
ssl_mode = config['main'].get('ssl_mode', None) or config['connection'].get('default_ssl_mode', None)
14+
def normalize_ssl_mode(
15+
config: ConfigObj,
16+
config_without_package_defaults: ConfigObj,
17+
) -> tuple[str | None, str | None]:
18+
error_notice: str | None = None
19+
ssl_mode: str | None = None
20+
21+
if 'main' in config_without_package_defaults and 'ssl_mode' in config_without_package_defaults['main']:
22+
# migration with notice added with mycli 2.0.0 in 2026-07
23+
# todo: entirely remove support for ssl_mode in [main]
24+
error_notice = (
25+
'Mycli 2.0 migration: automatically moving ssl_mode under [main] to default_ssl_mode under [connection] in ~/.myclirc .'
26+
)
27+
28+
ssl_mode = config_without_package_defaults['main']['ssl_mode']
29+
30+
config_without_package_defaults.encoding = 'utf-8'
31+
if 'connection' not in config_without_package_defaults:
32+
config_without_package_defaults['connection'] = {}
33+
if config_without_package_defaults['connection'].get('default_ssl_mode', None) in (None, ''):
34+
config_without_package_defaults['connection']['default_ssl_mode'] = ssl_mode
35+
else:
36+
ssl_mode = config_without_package_defaults['connection'].get('default_ssl_mode')
37+
error_notice += f'\nBut connection.default_ssl_mode already existed, with the value: "{ssl_mode}".'
38+
del config_without_package_defaults['main']['ssl_mode']
39+
config_without_package_defaults.write()
40+
41+
if ssl_mode is None and 'default_ssl_mode' in config['connection']:
42+
ssl_mode = config['connection']['default_ssl_mode']
1643
if ssl_mode not in ('auto', 'on', 'off', None):
17-
return None, f'Invalid config option provided for ssl_mode ({ssl_mode}); ignoring.'
18-
return ssl_mode, None
44+
error_notice = f'Invalid config option provided for ssl_mode ({ssl_mode}); ignoring.'
45+
return None, error_notice
46+
return ssl_mode, error_notice
1947

2048

2149
def configure_prompt_state(

mycli/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def __init__(
132132
self.binary_display = c['main'].get('binary_display')
133133
self.llm_prompt_field_truncate, self.llm_prompt_section_truncate = llm_prompt_truncation(c)
134134

135-
self.ssl_mode, ssl_mode_error = normalize_ssl_mode(c)
135+
self.ssl_mode, ssl_mode_error = normalize_ssl_mode(c, self.config_without_package_defaults)
136136
if ssl_mode_error:
137137
self.echo(ssl_mode_error, err=True, fg="red")
138138

mycli/client_connection.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,34 @@ def connect(
7676

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

79-
# default_character_set doesn't check in self.config_without_package_defaults, because the
80-
# option already existed before the my.cnf deprecation. For the same reason,
81-
# default_character_set can be in [connection] or [main].
8279
if not character_set:
83-
if 'default_character_set' in self.config['connection']:
80+
if 'main' in self.config_without_package_defaults and 'default_character_set' in self.config_without_package_defaults['main']:
81+
# migration with notice added with mycli 2.0.0 in 2026-07
82+
# todo: entirely remove support for default_character_set in [main]
83+
click.secho(
84+
'Mycli 2.0 migration: automatically moving default_character_set from [main] to [connection] in ~/.myclirc .',
85+
err=True,
86+
fg='red',
87+
)
88+
character_set = self.config_without_package_defaults['main']['default_character_set']
89+
90+
self.config_without_package_defaults.encoding = 'utf-8'
91+
if 'connection' not in self.config_without_package_defaults:
92+
self.config_without_package_defaults['connection'] = {}
93+
if self.config_without_package_defaults['connection'].get('default_character_set', None) in (None, ''):
94+
self.config_without_package_defaults['connection']['default_character_set'] = character_set
95+
else:
96+
character_set = self.config_without_package_defaults["connection"].get("default_character_set")
97+
click.secho(
98+
f'But connection.default_character_set already existed, with the value: "{character_set}".',
99+
err=True,
100+
fg='red',
101+
)
102+
del self.config_without_package_defaults['main']['default_character_set']
103+
self.config_without_package_defaults.write()
104+
105+
if not character_set and 'default_character_set' in self.config['connection']:
84106
character_set = self.config['connection']['default_character_set']
85-
elif 'default_character_set' in self.config['main']:
86-
character_set = self.config['main']['default_character_set']
87107
if not character_set:
88108
character_set = DEFAULT_CHARSET
89109

test/pytests/test_app_state.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,60 @@ def __init__(self, login_path: str | None = None) -> None:
1818

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

23-
assert normalize_ssl_mode(config) == (ssl_mode, None)
24+
assert normalize_ssl_mode(config, config_wo) == (ssl_mode, None)
2425

2526

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

29-
assert normalize_ssl_mode(config) == ('on', None)
31+
assert normalize_ssl_mode(config, config_wo) == ('on', None)
32+
33+
34+
def test_normalize_ssl_mode_returns_none_when_not_configured() -> None:
35+
config = ConfigObj({'main': {}, 'connection': {}})
36+
config_wo = ConfigObj({'main': {}, 'connection': {}})
37+
38+
assert normalize_ssl_mode(config, config_wo) == (None, None)
39+
40+
41+
def test_normalize_ssl_mode_migrates_deprecated_main_value() -> None:
42+
config = ConfigObj({'main': {}, 'connection': {'default_ssl_mode': 'off'}})
43+
config_wo = ConfigObj({'main': {'ssl_mode': 'on'}})
44+
45+
ssl_mode, warning = normalize_ssl_mode(config, config_wo)
46+
47+
assert ssl_mode == 'on'
48+
assert (
49+
warning == 'Mycli 2.0 migration: automatically moving ssl_mode under [main] to default_ssl_mode under [connection] in ~/.myclirc .'
50+
)
51+
assert config_wo['connection']['default_ssl_mode'] == 'on'
52+
assert 'ssl_mode' not in config_wo['main']
53+
54+
55+
def test_normalize_ssl_mode_uses_existing_connection_value_when_migrating() -> None:
56+
config = ConfigObj({'main': {}, 'connection': {'default_ssl_mode': 'off'}})
57+
config_wo = ConfigObj({'main': {'ssl_mode': 'on'}, 'connection': {'default_ssl_mode': 'off'}})
58+
59+
ssl_mode, warning = normalize_ssl_mode(config, config_wo)
60+
61+
assert ssl_mode == 'off'
62+
assert warning == (
63+
'Mycli 2.0 migration: automatically moving ssl_mode under [main] to default_ssl_mode under [connection] in ~/.myclirc .'
64+
'\nBut connection.default_ssl_mode already existed, with the value: "off".'
65+
)
66+
assert config_wo['connection']['default_ssl_mode'] == 'off'
67+
assert 'ssl_mode' not in config_wo['main']
3068

3169

3270
def test_normalize_ssl_mode_reports_invalid_values() -> None:
33-
config = ConfigObj({'main': {'ssl_mode': 'required'}, 'connection': {'default_ssl_mode': 'off'}})
71+
config = ConfigObj({'main': {'ssl_mode': 'required'}, 'connection': {'default_ssl_mode': 'required'}})
72+
config_wo = ConfigObj()
3473

35-
ssl_mode, warning = normalize_ssl_mode(config)
74+
ssl_mode, warning = normalize_ssl_mode(config, config_wo)
3675

3776
assert ssl_mode is None
3877
assert warning == 'Invalid config option provided for ssl_mode (required); ignoring.'

test/pytests/test_checkup.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ def test_configuration_checkup_reports_missing_unsupported_and_deprecated(capsys
133133
'main': {
134134
'present': '',
135135
'unsupported_item': '',
136-
'default_character_set': '',
137136
},
138137
'unsupported_section': {
139138
'anything': '',
@@ -162,9 +161,6 @@ def test_configuration_checkup_reports_missing_unsupported_and_deprecated(capsys
162161
assert '### Unsupported in user ~/.myclirc:' in output
163162
assert 'The entire section:\n\n [unsupported_section]\n' in output
164163
assert 'The item:\n\n [main]\n unsupported_item =' in output
165-
assert '### Deprecated in user ~/.myclirc:' in output
166-
assert ' [main]\n default_character_set' in output
167-
assert ' [connection]\n default_character_set' in output
168164
assert f'{checkup.REPO_URL}/blob/main/mycli/myclirc' in output
169165

170166

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

206201

202+
def test_configuration_checkup_reports_deprecated_transition(capsys) -> None:
203+
mycli = SimpleNamespace(
204+
config={
205+
'main': {},
206+
},
207+
config_without_package_defaults={
208+
'main': {
209+
'ssl_mode': '',
210+
},
211+
},
212+
config_without_user_options={
213+
'main': {},
214+
},
215+
)
216+
217+
checkup._configuration_checkup(mycli)
218+
output = capsys.readouterr().out
219+
220+
assert '### Deprecated in user ~/.myclirc:' in output
221+
assert 'It is recommended to transition:\n\n [main]\n ssl_mode\n\nto\n\n [connection]\n default_ssl_mode' in output
222+
assert '### Unsupported in user ~/.myclirc:' not in output
223+
assert f'{checkup.REPO_URL}/blob/main/mycli/myclirc' in output
224+
225+
207226
def test_configuration_checkup_up_to_date(capsys) -> None:
208227
mycli = SimpleNamespace(
209228
config={

test/pytests/test_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def test_init_reports_invalid_ssl_mode(monkeypatch: pytest.MonkeyPatch, tmp_path
3131
myclirc = write_myclirc(
3232
tmp_path,
3333
"""
34-
[main]
35-
ssl_mode = invalid
34+
[connection]
35+
default_ssl_mode = invalid
3636
""",
3737
)
3838

test/pytests/test_client_connection.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ def echo(self, *args: Any, **kwargs: Any) -> None:
5252
self.echo_calls.append((args, kwargs))
5353

5454

55+
class WritableConfig(dict[str, Any]):
56+
encoding: str | None = None
57+
58+
def __init__(self, value: dict[str, Any]) -> None:
59+
super().__init__(value)
60+
self.write_calls = 0
61+
62+
def write(self) -> None:
63+
self.write_calls += 1
64+
65+
5566
class FakeSQLExecute:
5667
calls: list[dict[str, Any]] = []
5768
effects: list[Any] = []
@@ -148,12 +159,70 @@ def test_connect_uses_character_set_from_connection_config() -> None:
148159
assert FakeSQLExecute.calls[-1]['character_set'] == 'utf16'
149160

150161

151-
def test_connect_uses_character_set_from_main_config() -> None:
152-
client = DummyClient(config={'main': {'default_character_set': 'utf32'}, 'connection': {}})
162+
def test_connect_migrates_deprecated_character_set_from_main_config(
163+
monkeypatch: pytest.MonkeyPatch,
164+
) -> None:
165+
config_wo = WritableConfig({'main': {'default_character_set': 'utf32'}})
166+
client = DummyClient(
167+
config={'main': {}, 'connection': {'default_character_set': 'utf16'}},
168+
config_without_package_defaults=config_wo,
169+
)
170+
secho_calls: list[tuple[str, dict[str, Any]]] = []
171+
monkeypatch.setattr(
172+
client_connection.click,
173+
'secho',
174+
lambda message, **kwargs: secho_calls.append((message, kwargs)),
175+
)
153176

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

156179
assert FakeSQLExecute.calls[-1]['character_set'] == 'utf32'
180+
assert config_wo.encoding == 'utf-8'
181+
assert config_wo['connection']['default_character_set'] == 'utf32'
182+
assert 'default_character_set' not in config_wo['main']
183+
assert config_wo.write_calls == 1
184+
assert secho_calls == [
185+
(
186+
'Mycli 2.0 migration: automatically moving default_character_set from [main] to [connection] in ~/.myclirc .',
187+
{'err': True, 'fg': 'red'},
188+
)
189+
]
190+
191+
192+
def test_connect_uses_existing_connection_character_set_when_migrating(
193+
monkeypatch: pytest.MonkeyPatch,
194+
) -> None:
195+
config_wo = WritableConfig({
196+
'main': {'default_character_set': 'utf32'},
197+
'connection': {'default_character_set': 'utf16'},
198+
})
199+
client = DummyClient(
200+
config={'main': {}, 'connection': {'default_character_set': 'latin1'}},
201+
config_without_package_defaults=config_wo,
202+
)
203+
secho_calls: list[tuple[str, dict[str, Any]]] = []
204+
monkeypatch.setattr(
205+
client_connection.click,
206+
'secho',
207+
lambda message, **kwargs: secho_calls.append((message, kwargs)),
208+
)
209+
210+
client.connect(host='db', port=3307)
211+
212+
assert FakeSQLExecute.calls[-1]['character_set'] == 'utf16'
213+
assert config_wo['connection']['default_character_set'] == 'utf16'
214+
assert 'default_character_set' not in config_wo['main']
215+
assert config_wo.write_calls == 1
216+
assert secho_calls == [
217+
(
218+
'Mycli 2.0 migration: automatically moving default_character_set from [main] to [connection] in ~/.myclirc .',
219+
{'err': True, 'fg': 'red'},
220+
),
221+
(
222+
'But connection.default_character_set already existed, with the value: "utf16".',
223+
{'err': True, 'fg': 'red'},
224+
),
225+
]
157226

158227

159228
def test_connect_uses_default_character_set_when_none_configured() -> None:

0 commit comments

Comments
 (0)