diff --git a/changelog.md b/changelog.md index 585fb5bc..921d952a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,11 @@ Upcoming (TBD) ============== +Breaking Changes +-------- +* Remove support for `my.cnf` vendor MySQL client option files. + + Internal --------- * Improve test coverage for DSN variable expansion. diff --git a/mycli/app_state.py b/mycli/app_state.py index e3cdd9ef..4907951c 100644 --- a/mycli/app_state.py +++ b/mycli/app_state.py @@ -1,12 +1,11 @@ from __future__ import annotations -from collections import defaultdict import re from typing import TYPE_CHECKING, Any from configobj import ConfigObj -from mycli.config import str_to_bool, strip_matching_quotes +from mycli.config import strip_matching_quotes if TYPE_CHECKING: from mycli.client import MyCli @@ -19,21 +18,13 @@ def normalize_ssl_mode(config: ConfigObj) -> tuple[str | None, str | None]: return ssl_mode, None -def ensure_my_cnf_sections(my_cnf: ConfigObj) -> None: - if not my_cnf.get('client'): - my_cnf['client'] = {} - if not my_cnf.get('mysqld'): - my_cnf['mysqld'] = {} - - def configure_prompt_state( mycli: MyCli, config: ConfigObj, prompt: str | None, - prompt_cnf: str | None, toolbar_format: str | None, ) -> None: - mycli.prompt_format = prompt or prompt_cnf or config['main']['prompt'] or mycli.default_prompt + mycli.prompt_format = prompt or config['main']['prompt'] or mycli.default_prompt mycli.prompt_lines = 0 mycli.multiline_continuation_char = config['main']['prompt_continuation'] mycli.toolbar_format = toolbar_format or config['main']['toolbar'] @@ -61,47 +52,22 @@ def llm_prompt_truncation(config: ConfigObj) -> tuple[int, int]: class AppStateMixin: - defaults_suffix: str | None login_path: str | None - def read_my_cnf(self, cnf: ConfigObj, keys: list[str]) -> dict[str, Any]: - sections = ['client', 'mysqld'] - key_transformations = { - 'mysqld': { - 'socket': 'default_socket', - 'port': 'default_port', - 'user': 'default_user', - }, - } - - if self.login_path and self.login_path != 'client': - sections.append(self.login_path) - - if self.defaults_suffix: - sections.extend([sect + self.defaults_suffix for sect in sections]) - - configuration: dict[str, Any] = defaultdict(lambda: None) - for key in keys: - for section in cnf: - if section not in sections or key not in cnf[section]: - continue - new_key = key_transformations.get(section, {}).get(key) or key - configuration[new_key] = strip_matching_quotes(cnf[section][key]) - - return configuration - - def merge_ssl_with_cnf(self, ssl: dict[str, Any], cnf: dict[str, Any]) -> dict[str, Any]: - merged = {} - merged.update(ssl) - prefix = 'ssl-' - for key, value in cnf.items(): - if not key.startswith(prefix): - continue - if value is None: + def read_mylogin_cnf(self, cnf: ConfigObj) -> dict[str, Any]: + allowed_keys = [ + 'user', + 'password', + 'host', + 'port', + 'socket', + ] + configuration: dict[str, Any] = dict.fromkeys(allowed_keys) + for section in cnf: + if section != self.login_path: continue - if key == 'ssl-verify-server-cert': - merged['check_hostname'] = str_to_bool(value) - else: - merged[key[len(prefix) :]] = value + for key in allowed_keys: + if key in cnf[section]: + configuration[key] = strip_matching_quotes(cnf[section][key]) - return merged + return configuration diff --git a/mycli/cli_runner.py b/mycli/cli_runner.py index 934692c5..4211cfa0 100644 --- a/mycli/cli_runner.py +++ b/mycli/cli_runner.py @@ -3,14 +3,13 @@ import os import re import sys -from textwrap import dedent from typing import TYPE_CHECKING, Any, Callable from urllib.parse import parse_qs, unquote, urlparse import click from mycli.config import str_to_bool -from mycli.constants import EMPTY_PASSWORD_FLAG_SENTINEL, ISSUES_URL, REPO_URL +from mycli.constants import EMPTY_PASSWORD_FLAG_SENTINEL, ISSUES_URL from mycli.main_modes.batch import main_batch_from_stdin, main_batch_with_progress_bar, main_batch_without_progress_bar from mycli.main_modes.checkup import main_checkup from mycli.main_modes.execute import main_execute_from_cli @@ -103,8 +102,6 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non prompt=cli_args.prompt, toolbar_format=cli_args.toolbar, logfile=cli_args.logfile, - defaults_suffix=cli_args.defaults_group_suffix, - defaults_file=cli_args.defaults_file, login_path=cli_args.login_path, auto_vertical_output=cli_args.auto_vertical_output, warn=cli_args.warn, @@ -392,82 +389,6 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non use_keyring = str_to_bool(cli_args.use_keyring) reset_keyring = False - # todo: removeme after a period of transition - for tup in [ - ('client', 'prompt', 'prompt', 'main', 'prompt'), - ('client', 'pager', 'pager', 'main', 'pager'), - ('client', 'skip-pager', 'skip-pager', 'main', 'enable_pager'), - # this is a white lie, because default_character_set can actually be read from the package config - ('client', 'default-character-set', 'default-character-set', 'connection', 'default_character_set'), - # local-infile can be read from both sections - ('mysqld', 'local-infile', 'local-infile', 'connection', 'default_local_infile'), - ('client', 'local-infile', 'local-infile', 'connection', 'default_local_infile'), - ('mysqld', 'loose-local-infile', 'loose-local-infile', 'connection', 'default_local_infile'), - ('client', 'loose-local-infile', 'loose-local-infile', 'connection', 'default_local_infile'), - # todo: in the future we should add default_port, etc, but only in .myclirc - # they are currently ignored in my.cnf - ('mysqld', 'default_socket', 'socket', 'connection', 'default_socket'), - ('client', 'ssl-ca', 'ssl-ca', 'connection', 'default_ssl_ca'), - ('client', 'ssl-cert', 'ssl-cert', 'connection', 'default_ssl_cert'), - ('client', 'ssl-key', 'ssl-key', 'connection', 'default_ssl_key'), - ('client', 'ssl-cipher', 'ssl-cipher', 'connection', 'default_ssl_cipher'), - ('client', 'ssl-verify-server-cert', 'ssl-verify-server-cert', 'connection', 'default_ssl_verify_server_cert'), - ]: - ( - mycnf_section_name, - mycnf_item_name, - printable_mycnf_item_name, - myclirc_section_name, - myclirc_item_name, - ) = tup - if str_to_bool(mycli.config['main'].get('my_cnf_transition_done', 'False')): - break - if ( - mycli.my_cnf[mycnf_section_name].get(mycnf_item_name) is None - and mycli.my_cnf[mycnf_section_name].get(mycnf_item_name.replace('-', '_')) is None - ): - continue - user_section = mycli.config_without_package_defaults.get(myclirc_section_name, {}) - if user_section.get(myclirc_item_name) is None: - cnf_value = mycli.my_cnf[mycnf_section_name].get(mycnf_item_name) - if cnf_value is None: - cnf_value = mycli.my_cnf[mycnf_section_name].get(mycnf_item_name.replace('-', '_')) - click.secho( - dedent( - f""" - Reading configuration from my.cnf files is deprecated. - See {ISSUES_URL}/1490 . - The cause of this message is the following in a my.cnf file without a corresponding - ~/.myclirc entry: - - [{mycnf_section_name}] - {printable_mycnf_item_name} = {cnf_value} - - To suppress this message, remove the my.cnf item add or the following to ~/.myclirc: - - [{myclirc_section_name}] - {myclirc_item_name} = - - The ~/.myclirc setting will take precedence. In the future, the my.cnf will be ignored. - - Values are documented at {REPO_URL}/blob/main/mycli/myclirc . An - empty is generally accepted. - - To ignore all of this, set - - [main] - my_cnf_transition_done = True - - in ~/.myclirc. - - -------- - - """ - ), - err=True, - fg='yellow', - ) - mycli.connect( database=database, user=cli_args.user, diff --git a/mycli/client.py b/mycli/client.py index 073544f5..220dbb74 100644 --- a/mycli/client.py +++ b/mycli/client.py @@ -7,6 +7,7 @@ from typing import IO, Literal from cli_helpers.tabular_output import TabularOutputFormatter +from configobj import ConfigObj from prompt_toolkit.formatted_text import to_formatted_text from prompt_toolkit.shortcuts import PromptSession import sqlparse @@ -15,7 +16,6 @@ AppStateMixin, configure_prompt_state, destructive_keywords_from_config, - ensure_my_cnf_sections, llm_prompt_truncation, normalize_ssl_mode, ) @@ -24,7 +24,13 @@ from mycli.client_query import ClientQueryMixin from mycli.clistyle import style_factory_helpers, style_factory_ptoolkit from mycli.completion_refresher import CompletionRefresher -from mycli.config import get_mylogin_cnf_path, open_mylogin_cnf, read_config_files, write_default_config +from mycli.config import ( + get_mylogin_cnf_path, + open_mylogin_cnf, + read_config_file, + read_config_files, + write_default_config, +) from mycli.constants import DEFAULT_PROMPT from mycli.main_modes import repl as repl_package from mycli.output import OutputMixin @@ -44,19 +50,10 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix default_prompt = DEFAULT_PROMPT default_prompt_splitln = "\\u@\\h\\n(\\t):\\d>" max_len_prompt = 45 - defaults_suffix = None prompt_lines: int sqlexecute: SQLExecute | None numeric_alignment: str - # In order of being loaded. Files lower in list override earlier ones. - cnf_files: list[str | IO[str]] = [ - "/etc/my.cnf", - "/etc/mysql/my.cnf", - "/usr/local/etc/my.cnf", - os.path.expanduser("~/.my.cnf"), - ] - # check XDG_CONFIG_HOME exists and not an empty string xdg_config_home = os.environ.get("XDG_CONFIG_HOME", "~/.config") system_config_files: list[str | IO[str]] = [ @@ -72,8 +69,6 @@ def __init__( prompt: str | None = None, toolbar_format: str | None = None, logfile: TextIOWrapper | Literal[False] | None = None, - defaults_suffix: str | None = None, - defaults_file: str | None = None, login_path: str | None = None, auto_vertical_output: bool = False, warn: bool | None = None, @@ -83,7 +78,6 @@ def __init__( ) -> None: self.sqlexecute = sqlexecute self.logfile = logfile - self.defaults_suffix = defaults_suffix self.login_path = login_path self.toolbar_error_message: str | None = None self.prompt_session: PromptSession | None = None @@ -92,23 +86,13 @@ def __init__( self.sandbox_mode: bool = False self.checkpoint: IO | None = None - # self.cnf_files is a class variable that stores the list of mysql - # config files to read in at launch. - # If defaults_file is specified then override the class variable with - # defaults_file. - if defaults_file: - self.cnf_files = [defaults_file] - # Load config. config_files: list[str | IO[str]] = self.system_config_files + [myclirc] + [self.pwd_config_file] c = self.config = read_config_files(config_files) - # this parallel config exists to - # * compare with my.cnf - # * support the --checkup feature - # todo: after removing my.cnf, create the parallel configs only when --checkup is set + # only needed in --checkup mode. todo: only load when needed self.config_without_package_defaults = read_config_files(config_files, ignore_package_defaults=True) - # this parallel config exists to compare with my.cnf support the --checkup feature + # only needed in --checkup mode. todo: only load when needed self.config_without_user_options = read_config_files(config_files, ignore_user_options=True) self.multi_line = c["main"].as_bool("multi_line") self.key_bindings = c["main"]["key_bindings"] @@ -201,20 +185,16 @@ def __init__( self.register_special_commands() # Load .mylogin.cnf if it exists. - mylogin_cnf_path = get_mylogin_cnf_path() - if mylogin_cnf_path: - mylogin_cnf = open_mylogin_cnf(mylogin_cnf_path) - if mylogin_cnf_path and mylogin_cnf: - # .mylogin.cnf gets read last, even if defaults_file is specified. - self.cnf_files.append(mylogin_cnf) - elif mylogin_cnf_path and not mylogin_cnf: - # There was an error reading the login path file. + self.mylogin_cnf = ConfigObj() + mylogin_cnf_h = None + if mylogin_cnf_path := get_mylogin_cnf_path(): + mylogin_cnf_h = open_mylogin_cnf(mylogin_cnf_path) + if mylogin_cnf_h: + self.mylogin_cnf = read_config_file(mylogin_cnf_h, list_values=False) or ConfigObj() + else: print("Error: Unable to read login path file.") - self.my_cnf = read_config_files(self.cnf_files, list_values=False) - ensure_my_cnf_sections(self.my_cnf) - prompt_cnf = self.read_my_cnf(self.my_cnf, ["prompt"])["prompt"] - configure_prompt_state(self, c, prompt, prompt_cnf, toolbar_format) + configure_prompt_state(self, c, prompt, toolbar_format) self.prompt_session = None self.destructive_keywords = destructive_keywords_from_config(c) special.set_destructive_keywords(self.destructive_keywords) diff --git a/mycli/client_connection.py b/mycli/client_connection.py index 2eba7b05..c09c2c70 100644 --- a/mycli/client_connection.py +++ b/mycli/client_connection.py @@ -31,7 +31,7 @@ class ClientConnectionMixin: if TYPE_CHECKING: - my_cnf: Any + mylogin_cnf: Any config: Any config_without_package_defaults: Any keepalive_ticks: int | None @@ -39,8 +39,7 @@ class ClientConnectionMixin: sqlexecute: Any logger: Any - def read_my_cnf(self, files: Any, keys: list[str]) -> dict[str, Any]: ... - def merge_ssl_with_cnf(self, ssl_config: dict[str, Any], cnf: dict[str, Any]) -> dict[str, Any] | None: ... + def read_mylogin_cnf(self, cnf: Any) -> dict[str, Any]: ... def echo(self, *args: Any, **kwargs: Any) -> None: ... def connect( @@ -65,31 +64,11 @@ def connect( reset_keyring: bool | None = None, keepalive_ticks: int | None = None, ) -> None: - cnf = { - "database": None, - "user": None, - "password": None, - "host": None, - "port": None, - "socket": None, - "default_socket": None, - "default-character-set": None, - "local-infile": None, - "loose-local-infile": None, - "ssl-ca": None, - "ssl-cert": None, - "ssl-key": None, - "ssl-cipher": None, - "ssl-verify-server-cert": None, - } - - cnf = self.read_my_cnf(self.my_cnf, list(cnf.keys())) - - # Fall back to config values only if user did not specify a value. - database = database or cnf["database"] - user = user or cnf["user"] or os.getenv("USER") - host = host or cnf["host"] - port = port or cnf["port"] + mylogin_cnf: dict[str, Any] = self.read_mylogin_cnf(self.mylogin_cnf) + # Fall back to .mylogin.cnf values only if user did not specify a value. + user = user or mylogin_cnf["user"] or os.getenv("USER") + host = host or mylogin_cnf["host"] + port = port or mylogin_cnf["port"] ssl_config: dict[str, Any] = ssl or {} user_connection_config = self.config_without_package_defaults.get('connection', {}) self.keepalive_ticks = keepalive_ticks @@ -98,28 +77,15 @@ def connect( if not int_port: int_port = DEFAULT_PORT if not host or host == DEFAULT_HOST: - socket = ( - socket - or user_connection_config.get("default_socket") - or cnf["socket"] - or cnf["default_socket"] - or guess_socket_location() - ) + socket = socket or user_connection_config.get("default_socket") or mylogin_cnf["socket"] or guess_socket_location() - passwd = passwd if isinstance(passwd, (str, int)) else cnf["password"] + 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']: character_set = self.config['connection']['default_character_set'] elif 'default_character_set' in self.config['main']: character_set = self.config['main']['default_character_set'] - elif 'default_character_set' in cnf: - character_set = cnf['default_character_set'] - elif 'default-character-set' in cnf: - character_set = cnf['default-character-set'] if not character_set: character_set = DEFAULT_CHARSET @@ -128,10 +94,6 @@ def connect( for local_infile_option in ( local_infile, user_connection_config.get('default_local_infile'), - cnf['local_infile'], - cnf['local-infile'], - cnf['loose_local_infile'], - cnf['loose-local-infile'], False, ): try: @@ -140,37 +102,19 @@ def connect( except (TypeError, ValueError): pass - # temporary my.cnf override mappings - if 'default_ssl_ca' in user_connection_config: - cnf['ssl-ca'] = user_connection_config.get('default_ssl_ca') or None - if 'default_ssl_cert' in user_connection_config: - cnf['ssl-cert'] = user_connection_config.get('default_ssl_cert') or None - if 'default_ssl_key' in user_connection_config: - cnf['ssl-key'] = user_connection_config.get('default_ssl_key') or None - if 'default_ssl_cipher' in user_connection_config: - cnf['ssl-cipher'] = user_connection_config.get('default_ssl_cipher') or None - if 'default_ssl_verify_server_cert' in user_connection_config: - cnf['ssl-verify-server-cert'] = user_connection_config.get('default_ssl_verify_server_cert') or None - - # todo: rewrite the merge method using self.config['connection'] instead of cnf, after removing my.cnf support - ssl_config_or_none: dict[str, Any] | None = self.merge_ssl_with_cnf(ssl_config, cnf) - - # default_ssl_ca_path is not represented in my.cnf - if 'default_ssl_ca_path' in self.config['connection'] and (not ssl_config_or_none or not ssl_config_or_none.get('capath')): - if ssl_config_or_none is None: - ssl_config_or_none = {} - ssl_config_or_none['capath'] = self.config['connection']['default_ssl_ca_path'] or False + if 'default_ssl_ca_path' in self.config['connection'] and (not ssl_config or not ssl_config.get('capath')): + ssl_config['capath'] = self.config['connection']['default_ssl_ca_path'] or False # prune lone check_hostname=False if not any(v for v in ssl_config.values()): - ssl_config_or_none = None + ssl_config = {} # password hierarchy # 1. -p / --pass/--password CLI options # 2. --password-file CLI option # 3. envvar (MYSQL_PWD) # 4. DSN (mysql://user:password) - # 5. cnf (.my.cnf / etc) + # 5. .mylogin.cnf # 6. keyring keyring_identifier = f'{user}@{host}:{"" if socket else int_port}:{socket or ""}' @@ -199,7 +143,7 @@ def connect( "socket": socket, "character_set": character_set, "local_infile": use_local_infile, - "ssl": ssl_config_or_none, + "ssl": ssl_config, "ssh_user": ssh_user, "ssh_host": ssh_host, "ssh_port": int(ssh_port) if ssh_port else None, diff --git a/mycli/config.py b/mycli/config.py index a79b1021..1053d50e 100644 --- a/mycli/config.py +++ b/mycli/config.py @@ -50,34 +50,6 @@ def read_config_file(f: str | IO[str], list_values: bool = True) -> ConfigObj | return config -def get_included_configs(config_file: str | IO[str]) -> list[str | IO[str]]: - """Get a list of configuration files that are included into config_path - with !includedir directive. - - "Normal" configs should be passed as file paths. The only exception - is .mylogin which is decoded into a stream. However, it never - contains include directives and so will be ignored by this - function. - - """ - if not isinstance(config_file, str) or not os.path.isfile(config_file): - return [] - included_configs: list[str | IO[str]] = [] - - try: - with open(config_file) as f: - include_directives = filter(lambda s: s.startswith("!includedir"), f) - dirs_split = (s.strip().split()[-1] for s in include_directives) - dirs = filter(os.path.isdir, dirs_split) - for dir_ in dirs: - for filename in os.listdir(dir_): - if filename.endswith(".cnf"): - included_configs.append(os.path.join(dir_, filename)) - except (PermissionError, UnicodeDecodeError): - pass - return included_configs - - def read_config_files( files: list[str | IO[str]], list_values: bool = True, @@ -99,10 +71,6 @@ def read_config_files( _file = _files.pop(0) _config = read_config_file(_file, list_values=list_values) - # expand includes only if we were able to parse config - # (otherwise we'll just encounter the same errors again) - if config is not None: - _files = get_included_configs(_file) + _files if _config is not None: config.merge(_config) config.filename = _config.filename diff --git a/mycli/main.py b/mycli/main.py index 6bd1b253..5a4c298a 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -215,14 +215,6 @@ class CliArgs: is_flag=True, help='In batch mode, resume after replaying statements in the --checkpoint file.', ) - defaults_group_suffix: str | None = clickdc.option( - type=str, - help='Read MySQL config groups with the specified suffix.', - ) - defaults_file: str | None = clickdc.option( - type=click.Path(), - help='Only read MySQL options from the given file.', - ) myclirc: str = clickdc.option( type=click.Path(), default='~/.myclirc', diff --git a/mycli/myclirc b/mycli/myclirc index 9fadbc63..c00b18b6 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -204,9 +204,6 @@ enable_pager = True # Choose a specific pager pager = 'less' -# whether to show verbose warnings about the transition away from reading my.cnf -my_cnf_transition_done = False - # Whether to store and retrieve passwords from the system keyring. # See the documentation for https://pypi.org/project/keyring/ for your OS. # Note that the hostname is considered to be different if short or qualified. diff --git a/mycli/output.py b/mycli/output.py index eee1021a..8c70a41e 100644 --- a/mycli/output.py +++ b/mycli/output.py @@ -6,7 +6,7 @@ import itertools import os import shutil -from typing import Any, Generator, Literal, Protocol +from typing import Generator, Literal, Protocol from cli_helpers.tabular_output import TabularOutputFormatter, preprocessors from cli_helpers.tabular_output.output_formatter import MISSING_VALUE as DEFAULT_MISSING_VALUE @@ -37,9 +37,6 @@ class MyCliState(Protocol): - # Provided by AppStateMixin. - def read_my_cnf(self, cnf: ConfigObj, keys: list[str]) -> dict[str, Any]: ... - # Provided by OutputMixin itself; declared so cross-method calls type-check. def log_output(self, output: str | AnyFormattedText) -> None: ... def get_output_margin(self, status: str | None = None) -> int: ... @@ -56,7 +53,6 @@ class OutputMixin(MyCliState): toolbar_format: str redirect_formatter: TabularOutputFormatter config: ConfigObj - my_cnf: ConfigObj logfile: TextIOWrapper | Literal[False] | None prompt_session: PromptSession | None prompt_format: str @@ -177,19 +173,18 @@ def configure_pager(self) -> None: if not os.environ.get("LESS"): os.environ["LESS"] = "-RXF" - cnf = self.read_my_cnf(self.my_cnf, ["pager", "skip-pager"]) - cnf_pager = cnf["pager"] or self.config["main"]["pager"] + config_pager = self.config["main"]["pager"] - if WIN and cnf_pager == 'less' and not shutil.which(cnf_pager): - cnf_pager = 'more' + if WIN and config_pager == 'less' and not shutil.which(config_pager): + config_pager = 'more' - if cnf_pager: - special.set_pager(cnf_pager) + if config_pager: + special.set_pager(config_pager) self.explicit_pager = True else: self.explicit_pager = False - if cnf["skip-pager"] or not self.config["main"].as_bool("enable_pager"): + if not self.config["main"].as_bool("enable_pager"): special.disable_pager() def format_sqlresult( diff --git a/test/features/connection.feature b/test/features/connection.feature index b06935ea..eb54e02b 100644 --- a/test/features/connection.feature +++ b/test/features/connection.feature @@ -22,11 +22,6 @@ Feature: connect to a database: When we query "status" Then status contains "via UNIX socket" - Scenario: run mycli with my.cnf configuration - When we create my.cnf file - When we run mycli without arguments "host port user pass defaults_file" - Then we are logged in - Scenario: run mycli with mylogin.cnf configuration When we create mylogin.cnf file When we run mycli with arguments "login_path=test_login_path" without arguments "host port user pass defaults_file" diff --git a/test/features/environment.py b/test/features/environment.py index 04b2f1ff..30a508c5 100644 --- a/test/features/environment.py +++ b/test/features/environment.py @@ -3,7 +3,6 @@ import os import shutil import sys -from tempfile import NamedTemporaryFile import db_utils as dbutils import fixture_utils as fixutils @@ -11,7 +10,6 @@ from mycli.constants import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USER from steps.wrappers import run_cli, wait_prompt -from test.utils import TEMPFILE_PREFIX test_log_file = os.path.join(os.environ["HOME"], ".mycli.test.log") @@ -19,8 +17,6 @@ SELF_CONNECTING_FEATURES = ("test/features/connection.feature",) -MY_CNF_PATH = os.path.expanduser("~/.my.cnf") -MY_CNF_BACKUP_PATH = f"{MY_CNF_PATH}.backup" MYLOGIN_CNF_PATH = os.path.expanduser("~/.mylogin.cnf") MYLOGIN_CNF_BACKUP_PATH = f"{MYLOGIN_CNF_PATH}.backup" @@ -64,12 +60,10 @@ def before_all(context): "pager_boundary": "---boundary---", } - with NamedTemporaryFile(prefix=TEMPFILE_PREFIX, mode='w', delete=False) as my_cnf: - my_cnf.write( - f'[client]\npager={sys.executable} ' - f'{os.path.join(context.package_root, "test/features/wrappager.py")} {context.conf["pager_boundary"]}\n' - ) - context.conf["defaults-file"] = my_cnf.name + # todo: this line has no effect, and the pager is controlled from test/myclirc + os.environ['PAGER'] = ( + f'{sys.executable} {os.path.join(context.package_root, "test/features/wrappager.py")} {context.conf["pager_boundary"]}' + ) context.conf["myclirc"] = os.path.join(context.package_root, "test", "myclirc") context.cn = dbutils.create_db( @@ -114,9 +108,6 @@ def before_scenario(context, arg): run_cli(context) wait_prompt(context) - if os.path.exists(MY_CNF_PATH): - shutil.move(MY_CNF_PATH, MY_CNF_BACKUP_PATH) - if os.path.exists(MYLOGIN_CNF_PATH): shutil.move(MYLOGIN_CNF_PATH, MYLOGIN_CNF_BACKUP_PATH) @@ -139,9 +130,6 @@ def after_scenario(context, _): context.cli.sendcontrol("d") context.cli.expect_exact(pexpect.EOF, timeout=5) - if os.path.exists(MY_CNF_BACKUP_PATH): - shutil.move(MY_CNF_BACKUP_PATH, MY_CNF_PATH) - if os.path.exists(MYLOGIN_CNF_BACKUP_PATH): shutil.move(MYLOGIN_CNF_BACKUP_PATH, MYLOGIN_CNF_PATH) elif os.path.exists(MYLOGIN_CNF_PATH): diff --git a/test/features/steps/connection.py b/test/features/steps/connection.py index dbc1eb4d..732a0938 100644 --- a/test/features/steps/connection.py +++ b/test/features/steps/connection.py @@ -7,7 +7,7 @@ import wrappers from mycli.config import encrypt_mylogin_cnf -from test.features.environment import MY_CNF_PATH, MYLOGIN_CNF_PATH, get_db_name_from_context +from test.features.environment import MYLOGIN_CNF_PATH, get_db_name_from_context from test.features.steps.utils import parse_cli_args_to_dict from test.utils import HOST, PASSWORD, PORT, USER @@ -31,13 +31,6 @@ def status_contains(context, expression): context.atprompt = True -@when("we create my.cnf file") -def step_create_my_cnf_file(context): - my_cnf = f"[client]\nhost = {HOST}\nport = {PORT}\nuser = {USER}\npassword = {PASSWORD}\n" - with open(MY_CNF_PATH, "w") as f: - f.write(my_cnf) - - @when("we create mylogin.cnf file") def step_create_mylogin_cnf_file(context): os.environ.pop("MYSQL_TEST_LOGIN_FILE", None) diff --git a/test/myclirc b/test/myclirc index 176d1156..2d0268dd 100644 --- a/test/myclirc +++ b/test/myclirc @@ -202,10 +202,7 @@ keyword_casing = auto enable_pager = True # Choose a specific pager -pager = less - -# whether to show verbose warnings about the transition away from reading my.cnf -my_cnf_transition_done = False +pager = python test/features/wrappager.py ---boundary--- # Whether to store and retrieve passwords from the system keyring. # See the documentation for https://pypi.org/project/keyring/ for your OS. diff --git a/test/pytests/test_app_state.py b/test/pytests/test_app_state.py index c1f61aca..dee5363c 100644 --- a/test/pytests/test_app_state.py +++ b/test/pytests/test_app_state.py @@ -1,22 +1,18 @@ from __future__ import annotations -from typing import Any - from configobj import ConfigObj import pytest from mycli.app_state import ( AppStateMixin, destructive_keywords_from_config, - ensure_my_cnf_sections, llm_prompt_truncation, normalize_ssl_mode, ) class AppState(AppStateMixin): - def __init__(self, defaults_suffix: str | None = None, login_path: str | None = None) -> None: - self.defaults_suffix = defaults_suffix + def __init__(self, login_path: str | None = None) -> None: self.login_path = login_path @@ -42,16 +38,6 @@ def test_normalize_ssl_mode_reports_invalid_values() -> None: assert warning == 'Invalid config option provided for ssl_mode (required); ignoring.' -def test_ensure_my_cnf_sections_adds_missing_sections() -> None: - config = ConfigObj({'client': {'user': 'alice'}, 'extra': {'port': '3307'}}) - - ensure_my_cnf_sections(config) - - assert config['client'] == {'user': 'alice'} - assert config['mysqld'] == {} - assert config['extra'] == {'port': '3307'} - - def test_destructive_keywords_from_config_splits_non_empty_words() -> None: config = ConfigObj({'main': {'destructive_keywords': 'DROP DELETE UPDATE'}}) @@ -85,62 +71,38 @@ def test_llm_prompt_truncation_handles_missing_llm_section() -> None: assert llm_prompt_truncation(ConfigObj({'main': {}})) == (0, 0) -def test_read_my_cnf_reads_allowed_sections_and_strips_quotes() -> None: - app_state = AppState() - cnf = ConfigObj({ - 'client': {'host': '"db.example.com"', 'socket': '/tmp/client.sock'}, - 'mysqld': {'socket': "'/tmp/mysql.sock'", 'port': '3307', 'user': 'mysql'}, - 'ignored': {'host': 'ignored.example.com'}, - }) - - configuration = app_state.read_my_cnf(cnf, ['host', 'socket', 'port', 'user', 'password']) - - assert configuration == { - 'host': 'db.example.com', - 'socket': '/tmp/client.sock', - 'default_socket': '/tmp/mysql.sock', - 'default_port': '3307', - 'default_user': 'mysql', - } - assert configuration['password'] is None - - -def test_read_my_cnf_includes_login_path_and_suffix_sections() -> None: - app_state = AppState(defaults_suffix='test', login_path='work') +def test_read_mylogin_cnf_reads_login_path_only() -> None: + app_state = AppState(login_path='work') cnf = ConfigObj({ 'client': {'user': 'client-user'}, 'work': {'password': 'work-pass'}, 'clienttest': {'host': 'client-test-host'}, - 'worktest': {'database': 'work-test-db'}, + 'worktest': {'socket': 'work-test-socket'}, }) - configuration = app_state.read_my_cnf(cnf, ['user', 'password', 'host', 'database']) + configuration = app_state.read_mylogin_cnf(cnf) assert configuration == { - 'user': 'client-user', + 'user': None, 'password': 'work-pass', - 'host': 'client-test-host', - 'database': 'work-test-db', + 'host': None, + 'port': None, + 'socket': None, } -def test_merge_ssl_with_cnf_keeps_existing_ssl_and_adds_cnf_values() -> None: - app_state = AppState() - ssl: dict[str, Any] = {'ca': 'existing-ca.pem', 'cert': 'existing-cert.pem'} - cnf = { - 'ssl-ca': 'cnf-ca.pem', - 'ssl-key': 'client-key.pem', - 'ssl-verify-server-cert': 'ON', - 'ssl-empty': None, - 'host': 'db.example.com', - } +def test_read_mylogin_cnf_strips_quotes() -> None: + app_state = AppState(login_path='work') + cnf = ConfigObj({ + 'work': {'password': '"work-pass"'}, + }) - merged = app_state.merge_ssl_with_cnf(ssl, cnf) + configuration = app_state.read_mylogin_cnf(cnf) - assert merged == { - 'ca': 'cnf-ca.pem', - 'cert': 'existing-cert.pem', - 'key': 'client-key.pem', - 'check_hostname': True, + assert configuration == { + 'user': None, + 'password': 'work-pass', + 'host': None, + 'port': None, + 'socket': None, } - assert ssl == {'ca': 'existing-ca.pem', 'cert': 'existing-cert.pem'} diff --git a/test/pytests/test_cli_runner.py b/test/pytests/test_cli_runner.py index f7a65b5e..d7e9561f 100644 --- a/test/pytests/test_cli_runner.py +++ b/test/pytests/test_cli_runner.py @@ -47,7 +47,7 @@ def close(self) -> None: def default_config() -> dict[str, Any]: return { - 'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'true'}, + 'main': {'use_keyring': 'false'}, 'connection': {'default_keepalive_ticks': 0}, 'alias_dsn': {}, 'init-commands': {}, diff --git a/test/pytests/test_client.py b/test/pytests/test_client.py index 2c1d3d6a..d54a1ea4 100644 --- a/test/pytests/test_client.py +++ b/test/pytests/test_client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from io import StringIO, TextIOWrapper +from io import TextIOWrapper import os from pathlib import Path from types import SimpleNamespace @@ -44,17 +44,6 @@ def test_init_reports_invalid_ssl_mode(monkeypatch: pytest.MonkeyPatch, tmp_path assert echo_calls == [('Invalid config option provided for ssl_mode (invalid); ignoring.', {'err': True, 'fg': 'red'})] -def test_init_uses_defaults_file_for_mysql_config_files(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - patch_constructor_side_effects(monkeypatch) - defaults_file = tmp_path / 'defaults.cnf' - defaults_file.write_text('[client]\nuser = alice\n', encoding='utf-8') - myclirc = write_myclirc(tmp_path, '') - - cli = MyCli(defaults_file=str(defaults_file), myclirc=myclirc) - - assert cli.cnf_files == [str(defaults_file)] - - def test_init_honors_explicit_show_warnings(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: patch_constructor_side_effects(monkeypatch) show_warnings_calls: list[bool] = [] @@ -142,18 +131,6 @@ def test_init_reports_unreadable_mylogin_cnf(monkeypatch: pytest.MonkeyPatch, tm assert 'Error: Unable to read login path file.' in capsys.readouterr().out -def test_init_appends_readable_mylogin_cnf(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - patch_constructor_side_effects(monkeypatch) - mylogin_cnf = StringIO('[client]\nuser = alice\n') - monkeypatch.setattr(client_module, 'get_mylogin_cnf_path', lambda: '/tmp/mylogin.cnf') - monkeypatch.setattr(client_module, 'open_mylogin_cnf', lambda path: mylogin_cnf) - myclirc = write_myclirc(tmp_path, '') - - cli = MyCli(myclirc=myclirc) - - assert cli.cnf_files[-1] is mylogin_cnf - - def test_close_stops_schema_prefetcher_and_closes_sqlexecute() -> None: cli = MyCli.__new__(MyCli) stopped: list[bool] = [] diff --git a/test/pytests/test_client_connection.py b/test/pytests/test_client_connection.py index d3912878..91d7acdc 100644 --- a/test/pytests/test_client_connection.py +++ b/test/pytests/test_client_connection.py @@ -35,7 +35,7 @@ def __init__( config_without_package_defaults: dict[str, Any] | None = None, ) -> None: self.cnf = cnf or default_cnf() - self.my_cnf = object() + self.mylogin_cnf = object() self.config = config or {'main': {}, 'connection': {}} self.config_without_package_defaults = config_without_package_defaults or {} self.keepalive_ticks: int | None = None @@ -43,16 +43,11 @@ def __init__( self.sqlexecute: Any = None self.logger = DummyLogger() self.echo_calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = [] - self.ssl_merge_calls: list[tuple[dict[str, Any], dict[str, Any]]] = [] - def read_my_cnf(self, files: Any, keys: list[str]) -> dict[str, Any]: - assert files is self.my_cnf + def read_mylogin_cnf(self, cnf: Any) -> dict[str, Any]: + assert cnf is self.mylogin_cnf return dict(self.cnf) - def merge_ssl_with_cnf(self, ssl_config: dict[str, Any], cnf: dict[str, Any]) -> dict[str, Any] | None: - self.ssl_merge_calls.append((dict(ssl_config), dict(cnf))) - return dict(ssl_config) if ssl_config else None - def echo(self, *args: Any, **kwargs: Any) -> None: self.echo_calls.append((args, kwargs)) @@ -143,7 +138,7 @@ def test_import_swallows_missing_pwd_module(monkeypatch: pytest.MonkeyPatch) -> def test_connect_defaults_to_port_socket_and_config_character_set( monkeypatch: pytest.MonkeyPatch, ) -> None: - client = DummyClient(config={'main': {'default_character_set': 'latin1'}, 'connection': {}}) + client = DummyClient(config={'connection': {'default_character_set': 'latin1'}, 'main': {}}) monkeypatch.setenv('USER', 'env_user') monkeypatch.setattr(client_connection, 'guess_socket_location', lambda: '/tmp/mysql.sock') monkeypatch.setattr(client_connection, 'WIN', True) @@ -155,17 +150,7 @@ def test_connect_defaults_to_port_socket_and_config_character_set( assert call['port'] == 3306 assert call['socket'] == '/tmp/mysql.sock' assert call['character_set'] == 'latin1' - assert call['ssl'] is None - - -def test_connect_uses_character_set_from_cnf_default_character_set() -> None: - cnf = default_cnf() - cnf['default_character_set'] = 'utf8mb4' - client = DummyClient(cnf=cnf) - - client.connect(host='db', port=3307) - - assert FakeSQLExecute.calls[-1]['character_set'] == 'utf8mb4' + assert call['ssl'] == {} def test_connect_uses_character_set_from_connection_config() -> None: @@ -176,15 +161,12 @@ def test_connect_uses_character_set_from_connection_config() -> None: assert FakeSQLExecute.calls[-1]['character_set'] == 'utf16' -def test_connect_uses_character_set_from_cnf_hyphenated_key() -> None: - cnf = default_cnf() - del cnf['default_character_set'] - cnf['default-character-set'] = 'latin1' - client = DummyClient(cnf=cnf) +def test_connect_uses_character_set_from_main_config() -> None: + client = DummyClient(config={'main': {'default_character_set': 'utf32'}, 'connection': {}}) client.connect(host='db', port=3307) - assert FakeSQLExecute.calls[-1]['character_set'] == 'latin1' + assert FakeSQLExecute.calls[-1]['character_set'] == 'utf32' def test_connect_uses_default_character_set_when_none_configured() -> None: @@ -203,38 +185,6 @@ def test_connect_accepts_local_infile_true() -> None: assert FakeSQLExecute.calls[-1]['local_infile'] is True -def test_connect_applies_ssl_overrides_from_user_connection_config() -> None: - client = DummyClient( - config_without_package_defaults={ - 'connection': { - 'default_ssl_ca': '/ca.pem', - 'default_ssl_cert': '/cert.pem', - 'default_ssl_key': '/key.pem', - 'default_ssl_cipher': 'AES256', - 'default_ssl_verify_server_cert': 'true', - } - } - ) - - client.connect(host='db', port=3307, ssl={'mode': 'on'}) - - merged_cnf = client.ssl_merge_calls[-1][1] - assert merged_cnf['ssl-ca'] == '/ca.pem' - assert merged_cnf['ssl-cert'] == '/cert.pem' - assert merged_cnf['ssl-key'] == '/key.pem' - assert merged_cnf['ssl-cipher'] == 'AES256' - assert merged_cnf['ssl-verify-server-cert'] == 'true' - - -def test_connect_adds_default_ssl_ca_path_when_merge_returns_none() -> None: - client = DummyClient(config={'main': {}, 'connection': {'default_ssl_ca_path': '/ca/path'}}) - client.merge_ssl_with_cnf = lambda ssl_config, cnf: None # type: ignore[method-assign] - - client.connect(host='db', port=3307, ssl={'mode': 'on'}) - - assert FakeSQLExecute.calls[-1]['ssl'] == {'capath': '/ca/path'} - - def test_connect_retrieves_password_from_keyring(monkeypatch: pytest.MonkeyPatch) -> None: client = DummyClient() get_password_calls: list[tuple[str, str]] = [] @@ -312,6 +262,14 @@ def test_connect_retries_without_ssl_for_auto_handshake_error() -> None: assert FakeSQLExecute.calls[1]['ssl'] is None +def test_connect_adds_default_ssl_ca_path() -> None: + client = DummyClient(config={'main': {}, 'connection': {'default_ssl_ca_path': '/ca/path'}}) + + client.connect(host='db', port=3307, ssl={'mode': 'on'}) + + assert FakeSQLExecute.calls[-1]['ssl'] == {'mode': 'on', 'capath': '/ca/path'} + + def test_connect_exits_when_ssl_retry_also_fails() -> None: client = DummyClient() FakeSQLExecute.effects = [ diff --git a/test/pytests/test_config.py b/test/pytests/test_config.py index 45b26ec4..b51717bd 100644 --- a/test/pytests/test_config.py +++ b/test/pytests/test_config.py @@ -2,7 +2,6 @@ """Unit tests for the mycli.config module.""" -import builtins from io import BytesIO, StringIO, TextIOWrapper import logging import os @@ -11,7 +10,6 @@ from tempfile import NamedTemporaryFile from types import SimpleNamespace -from configobj import ConfigObj import pytest from mycli import config as config_module @@ -19,13 +17,11 @@ _remove_pad, create_default_config, encrypt_mylogin_cnf, - get_included_configs, get_mylogin_cnf_path, log, open_mylogin_cnf, read_and_decrypt_mylogin_cnf, read_config_file, - read_config_files, str_to_bool, strip_matching_quotes, write_default_config, @@ -210,57 +206,6 @@ def raise_oserror(*_args, **_kwargs): assert "You don't have permission to read config file '/tmp/test.cnf'." in caplog.text -def test_get_included_configs_handles_paths_and_errors(tmp_path, monkeypatch) -> None: - include_dir = tmp_path / 'includes' - include_dir.mkdir() - expected = include_dir / 'included.cnf' - expected.write_text('[main]\nfoo = bar\n', encoding='utf8') - (include_dir / 'ignore.txt').write_text('skip', encoding='utf8') - - config_path = tmp_path / 'root.cnf' - config_path.write_text(f'!includedir {include_dir}\n', encoding='utf8') - - assert get_included_configs(BytesIO()) == [] - assert get_included_configs(str(tmp_path / 'missing.cnf')) == [] - assert get_included_configs(str(config_path)) == [str(expected)] - - monkeypatch.setattr(builtins, 'open', lambda *_args, **_kwargs: (_ for _ in ()).throw(PermissionError())) - assert get_included_configs(str(config_path)) == [] - - -def test_read_config_files_merges_includes_and_honors_flags(monkeypatch) -> None: - first_config = ConfigObj({'main': {'color': 'blue'}}) - first_config.filename = 'first.cnf' - included_config = ConfigObj({'main': {'pager': 'less'}}) - included_config.filename = 'included.cnf' - - monkeypatch.setattr(config_module, 'create_default_config', lambda list_values=True: ConfigObj({'default': {'a': '1'}})) - - def fake_read_config_file(filename, list_values=True): - if filename == 'first.cnf': - return first_config - if filename == 'included.cnf': - return included_config - return None - - monkeypatch.setattr(config_module, 'read_config_file', fake_read_config_file) - monkeypatch.setattr(config_module, 'get_included_configs', lambda filename: ['included.cnf'] if filename == 'first.cnf' else []) - - merged = read_config_files(['first.cnf']) - assert merged['default']['a'] == '1' - assert merged['main']['color'] == 'blue' - assert merged['main']['pager'] == 'less' - assert merged.filename == 'included.cnf' - - ignored_defaults = read_config_files(['first.cnf'], ignore_package_defaults=True) - assert 'default' not in ignored_defaults - assert ignored_defaults['main']['color'] == 'blue' - - untouched = read_config_files(['first.cnf'], ignore_user_options=True) - assert untouched == ConfigObj({'default': {'a': '1'}}) - assert 'main' not in untouched - - def test_create_and_write_default_config(tmp_path) -> None: default_config = create_default_config() assert 'main' in default_config diff --git a/test/pytests/test_main.py b/test/pytests/test_main.py index b16d3495..37ce7e2f 100644 --- a/test/pytests/test_main.py +++ b/test/pytests/test_main.py @@ -93,8 +93,6 @@ PASSWORD, "--myclirc", default_config_file, - "--defaults-file", - default_config_file, ] CLI_ARGS = CLI_ARGS_WITHOUT_DB + [TEST_DATABASE] @@ -617,8 +615,6 @@ def test_no_show_warnings_overrides_myclirc_setting(executor, tmp_path, monkeypa PASSWORD, '--myclirc', myclirc.name, - '--defaults-file', - default_config_file, TEST_DATABASE, ] @@ -998,7 +994,6 @@ def __init__(self, **args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = "auto" - self.my_cnf = {"client": {}, "mysqld": {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1258,7 +1253,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1311,7 +1305,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1364,7 +1357,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1422,7 +1414,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1481,7 +1472,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1550,7 +1540,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1632,7 +1621,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1705,7 +1693,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1774,7 +1761,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1829,7 +1815,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1898,7 +1883,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -1965,7 +1949,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -2028,7 +2011,6 @@ def __init__(self, **args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = "auto" - self.my_cnf = {"client": {}, "mysqld": {}} self.default_keepalive_ticks = 0 def connect(self, **args): @@ -2200,7 +2182,6 @@ class MockMyCli: config = { 'main': { 'use_keyring': 'False', - 'my_cnf_transition_done': 'True', }, 'connection': {}, } @@ -2211,7 +2192,6 @@ def __init__(self, **_args): self.main_formatter = Formatter() self.redirect_formatter = Formatter() self.ssl_mode = 'auto' - self.my_cnf = {'client': {}, 'mysqld': {}} self.default_keepalive_ticks = 0 self.config_without_package_defaults = {'connection': {}} @@ -2258,7 +2238,7 @@ def test_verbose_and_quiet_are_incompatible() -> None: 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'}, + 'main': {'use_keyring': 'false'}, 'connection': {'default_keepalive_ticks': 0}, 'alias_dsn': {}, } @@ -2409,7 +2389,7 @@ def test_on_completions_refreshed_updates_completer_and_invalidates_prompt() -> def test_click_entrypoint_callback_covers_dsn_list_init_commands(monkeypatch: pytest.MonkeyPatch) -> None: dummy_class = make_dummy_mycli_class( config={ - 'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'true'}, + 'main': {'use_keyring': 'false'}, 'connection': {'default_keepalive_ticks': 0}, 'alias_dsn': {'prod': 'mysql://u:p@h/db'}, 'alias_dsn.init-commands': {'prod': ['set a=1', 'set b=2']}, @@ -2432,7 +2412,7 @@ def test_click_entrypoint_callback_covers_dsn_list_init_commands(monkeypatch: py def test_click_entrypoint_callback_uses_batch_with_progress_path(monkeypatch: pytest.MonkeyPatch) -> None: dummy_class = make_dummy_mycli_class( config={ - 'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'true'}, + 'main': {'use_keyring': 'false'}, 'connection': {'default_keepalive_ticks': 0}, 'alias_dsn': {}, } @@ -2453,7 +2433,7 @@ def test_click_entrypoint_callback_uses_batch_with_progress_path(monkeypatch: py def test_click_entrypoint_callback_uses_batch_without_progress_path(monkeypatch: pytest.MonkeyPatch) -> None: dummy_class = make_dummy_mycli_class( config={ - 'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'true'}, + 'main': {'use_keyring': 'false'}, 'connection': {'default_keepalive_ticks': 0}, 'alias_dsn': {}, } @@ -2471,27 +2451,6 @@ def test_click_entrypoint_callback_uses_batch_without_progress_path(monkeypatch: assert excinfo.value.code == 13 -def test_click_entrypoint_callback_covers_mycnf_underscore_fallback(monkeypatch: pytest.MonkeyPatch) -> None: - click_lines: list[str] = [] - monkeypatch.setattr(click, 'secho', lambda message='', **kwargs: click_lines.append(str(message))) - monkeypatch.setattr(sys, 'stdin', SimpleNamespace(isatty=lambda: True)) - monkeypatch.setattr(sys.stderr, 'isatty', lambda: False) - - dummy_class = make_dummy_mycli_class( - config={ - 'main': {'use_keyring': 'false', 'my_cnf_transition_done': 'false'}, - 'connection': {'default_keepalive_ticks': 0}, - 'alias_dsn': {}, - }, - my_cnf={'client': {'ssl_ca': '/tmp/ca.pem'}, 'mysqld': {}}, - config_without_package_defaults={'main': {}}, - ) - monkeypatch.setattr(main, 'MyCli', dummy_class) - - call_click_entrypoint_direct(main.CliArgs()) - assert any('ssl-ca = /tmp/ca.pem' in line for line in click_lines) - - def test_format_sqlresult_uses_redirect_formatter_when_redirected() -> None: cli = make_bare_mycli() cli.main_formatter = DummyFormatter() @@ -2533,7 +2492,6 @@ def test_get_last_query_returns_latest_query() -> None: def test_connect_reports_expired_password_login_error(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() - cli.my_cnf = {'client': {}, 'mysqld': {}} cli.config_without_package_defaults = {'connection': {}} cli.config = {'connection': {}, 'main': {}} cli.logger = cast(Any, DummyLogger()) @@ -2556,7 +2514,6 @@ class ExpiredPasswordSQLExecute(RecordingSQLExecute): def test_connect_sets_cli_sandbox_mode_when_sqlexecute_enters_sandbox(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() - cli.my_cnf = {'client': {}, 'mysqld': {}} cli.config_without_package_defaults = {'connection': {}} cli.config = {'connection': {}, 'main': {}} cli.logger = cast(Any, DummyLogger()) diff --git a/test/pytests/test_output.py b/test/pytests/test_output.py index 2cfa49f9..379f85be 100644 --- a/test/pytests/test_output.py +++ b/test/pytests/test_output.py @@ -268,9 +268,7 @@ def test_output_sends_buffer_to_pager_when_pager_is_explicit(monkeypatch: pytest def test_configure_pager_uses_more_for_missing_less_on_windows(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() - cli.my_cnf = ConfigObj({'client': {'pager': 'less'}}) - cli.config = ConfigObj({'main': {'pager': '', 'enable_pager': 'True'}}) - cli.read_my_cnf = lambda cnf, keys: {'pager': 'less', 'skip-pager': None} # type: ignore[assignment] + cli.config = ConfigObj({'main': {'pager': 'less', 'enable_pager': 'True'}}) pager_calls: list[str] = [] monkeypatch.setattr(output_module, 'WIN', True) monkeypatch.setattr(output_module.shutil, 'which', lambda value: None) @@ -282,11 +280,9 @@ def test_configure_pager_uses_more_for_missing_less_on_windows(monkeypatch: pyte assert pager_calls == ['more'] -def test_configure_pager_prefers_my_cnf_pager_and_sets_less(monkeypatch: pytest.MonkeyPatch) -> None: +def test_configure_pager_uses_myclirc_pager_and_sets_less(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() - cli.my_cnf = ConfigObj({'client': {'pager': 'my-pager'}}) cli.config = ConfigObj({'main': {'pager': 'config-pager', 'enable_pager': 'True'}}) - cli.read_my_cnf = lambda cnf, keys: {'pager': 'my-pager', 'skip-pager': None} # type: ignore[assignment] pager_calls: list[str] = [] disabled: list[bool] = [] monkeypatch.delenv('LESS', raising=False) @@ -295,25 +291,25 @@ def test_configure_pager_prefers_my_cnf_pager_and_sets_less(monkeypatch: pytest. OutputMixin.configure_pager(cli) - assert pager_calls == ['my-pager'] + assert pager_calls == ['config-pager'] assert disabled == [] assert cli.explicit_pager is True assert output_module.os.environ['LESS'] == '-RXF' -def test_configure_pager_disables_when_skip_pager_is_set(monkeypatch: pytest.MonkeyPatch) -> None: +def test_configure_pager_disables_pager_when_configured(monkeypatch: pytest.MonkeyPatch) -> None: cli = make_bare_mycli() - cli.my_cnf = ConfigObj({'client': {}}) - cli.config = ConfigObj({'main': {'pager': '', 'enable_pager': 'True'}}) - cli.read_my_cnf = lambda cnf, keys: {'pager': None, 'skip-pager': '1'} # type: ignore[assignment] + cli.config = ConfigObj({'main': {'pager': '', 'enable_pager': 'False'}}) + pager_calls: list[str] = [] disabled: list[bool] = [] - monkeypatch.setattr(output_module.special, 'set_pager', lambda value: None) + monkeypatch.setattr(output_module.special, 'set_pager', lambda value: pager_calls.append(value)) monkeypatch.setattr(output_module.special, 'disable_pager', lambda: disabled.append(True)) OutputMixin.configure_pager(cli) - assert cli.explicit_pager is False + assert pager_calls == [] assert disabled == [True] + assert cli.explicit_pager is False def test_format_sqlresult_uses_redirect_formatter_and_appends_preamble_postamble() -> None: diff --git a/test/utils.py b/test/utils.py index ec78f874..f47fb6dd 100644 --- a/test/utils.py +++ b/test/utils.py @@ -9,6 +9,7 @@ from types import SimpleNamespace from typing import Any, Callable, Literal, cast +from configobj import ConfigObj from packaging.version import Version from prompt_toolkit.formatted_text import ( ANSI, @@ -199,13 +200,13 @@ def make_bare_mycli() -> Any: cli.refresh_completions = lambda reset=False: [SQLResult(status='refresh')] # type: ignore[assignment] cli.reconnect = lambda database='': False # type: ignore[assignment] cli.checkpoint = None + cli.mylogin_cnf = ConfigObj() return cli def make_dummy_mycli_class( *, config: dict[str, Any] | None = None, - my_cnf: dict[str, Any] | None = None, config_without_package_defaults: dict[str, Any] | None = None, ) -> Any: class DummyMyCli: @@ -215,7 +216,6 @@ def __init__(self, **kwargs: Any) -> None: type(self).last_instance = self self.init_kwargs = dict(kwargs) self.config = config or {'main': {}, 'alias_dsn': {}} - self.my_cnf = my_cnf or {'client': {}, 'mysqld': {}} self.config_without_package_defaults = config_without_package_defaults or {} self.default_keepalive_ticks = 5 self.ssl_mode = None