Skip to content
Open
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
8 changes: 8 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
Upcoming (TBD)
==============

Features
---------
* Add a dependencies section to `--checkup`.


1.63.0 (2026/03/12)
==============

Expand Down
108 changes: 2 additions & 106 deletions mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
from mycli.key_bindings import mycli_bindings
from mycli.lexer import MyCliLexer
from mycli.packages import special
from mycli.packages.checkup import do_checkup
from mycli.packages.filepaths import dir_path_exists, guess_socket_location
from mycli.packages.hybrid_redirection import get_redirect_components, is_redirect_command
from mycli.packages.parseutils import is_destructive, is_dropping_database, is_valid_connection_scheme
Expand Down Expand Up @@ -2130,7 +2131,7 @@ def get_password_from_file(password_file: str | None) -> str | None:
)

if checkup:
do_config_checkup(mycli)
do_checkup(mycli)
sys.exit(0)

if csv and batch_format not in [None, 'csv']:
Expand Down Expand Up @@ -2688,110 +2689,5 @@ def read_ssh_config(ssh_config_path: str):
return ssh_config


def do_config_checkup(mycli: MyCli) -> None:
did_output_missing = False
did_output_unsupported = False
did_output_deprecated = False

print('\n### External executables:\n')
for executable in [
'less',
'fzf',
'pygmentize',
]:
if shutil.which(executable):
print(f'The "{executable}" executable was found — good!')
else:
print(f'The recommended "{executable}" executable was not found — some functionality will suffer.')

print('\n### Environment variables:\n')
for variable in [
'EDITOR',
'VISUAL',
]:
if value := os.environ.get(variable):
print(f'The ${variable} environment variable was set to "{value}" — good!')
else:
print(f'The ${variable} environment variable was not set — some functionality will suffer.')

indent = ' '
transitions = {
f'{indent}[main]\n{indent}default_character_set': f'{indent}[connection]\n{indent}default_character_set',
f'{indent}[main]\n{indent}ssl_mode': f'{indent}[connection]\n{indent}default_ssl_mode',
}
reverse_transitions = {v: k for k, v in transitions.items()}

if not list(mycli.config.keys()):
print('\n### Missing file:\n')
print('The local ~/,myclirc is missing or empty.\n')
did_output_missing = True
else:
for section_name in mycli.config:
if section_name not in mycli.config_without_package_defaults:
if not did_output_missing:
print('\n### Missing in user ~/.myclirc:\n')
print(f'The entire section:\n\n{indent}[{section_name}]\n')
did_output_missing = True
continue
for item_name in mycli.config[section_name]:
transition_key = f'{indent}[{section_name}]\n{indent}{item_name}'
if transition_key in reverse_transitions:
continue
if item_name not in mycli.config_without_package_defaults[section_name]:
if not did_output_missing:
print('\n### Missing in user ~/.myclirc:\n')
print(f'The item:\n\n{indent}[{section_name}]\n{indent}{item_name} =\n')
did_output_missing = True

for section_name in mycli.config_without_package_defaults:
if section_name not in mycli.config_without_user_options:
if not did_output_unsupported:
print('\n### Unsupported in user ~/.myclirc:\n')
did_output_unsupported = True
print(f'The entire section:\n\n{indent}[{section_name}]\n')
continue
for item_name in mycli.config_without_package_defaults[section_name]:
if section_name == 'colors' and item_name.startswith('sql.'):
# these are commented out in the package myclirc
continue
if section_name in [
'favorite_queries',
'init-commands',
'alias_dsn',
'alias_dsn.init-commands',
]:
# these are free-entry sections, so a comparison per item is not meaningful
continue
transition_key = f'{indent}[{section_name}]\n{indent}{item_name}'
if transition_key in transitions:
continue
if item_name not in mycli.config_without_user_options[section_name]:
if not did_output_unsupported:
print('\n### Unsupported in user ~/.myclirc:\n')
print(f'The item:\n\n{indent}[{section_name}]\n{indent}{item_name} =\n')
did_output_unsupported = True

for section_name in mycli.config_without_package_defaults:
if section_name not in mycli.config_without_user_options:
continue
for item_name in mycli.config_without_package_defaults[section_name]:
if section_name == 'colors' and item_name.startswith('sql.'):
# these are commented out in the package myclirc
continue
transition_key = f'{indent}[{section_name}]\n{indent}{item_name}'
if transition_key in transitions:
if not did_output_deprecated:
print('\n### Deprecated in user ~/.myclirc:\n')
transition_value = transitions[transition_key]
print(f'It is recommended to transition:\n\n{transition_key}\n\nto\n\n{transition_value}\n')
did_output_deprecated = True

if did_output_missing or did_output_unsupported or did_output_deprecated:
print(f'For more info on supported features, see the commentary and defaults at:\n\n * {REPO_URL}/blob/main/mycli/myclirc\n')
else:
print('\n### Configuration:\n')
print('User configuration all up to date!\n')


if __name__ == "__main__":
cli()
156 changes: 156 additions & 0 deletions mycli/packages/checkup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import importlib.metadata
import json
import os
import shutil
import sys
import urllib.error
import urllib.request

from mycli.constants import REPO_URL

PYPI_API_BASE = 'https://pypi.org/pypi'


def pypi_api_fetch(fragment: str) -> dict:
fragment = fragment.lstrip('/')
url = f'{PYPI_API_BASE}/{fragment}'
try:
with urllib.request.urlopen(url, timeout=5) as response:
return json.loads(response.read().decode('utf8'))
except urllib.error.URLError:
print(f'Failed to connect to PyPi on {url}', file=sys.stderr)
return {}


def _dependencies_checkup() -> None:
print('\n### Key Python dependencies:\n')
for dependency in [
'cli_helpers',
'click',
'prompt_toolkit',
'pymysql',
'tabulate',
]:
try:
installed_version = importlib.metadata.version(dependency)
except importlib.metadata.PackageNotFoundError:
installed_version = None
pypi_profile = pypi_api_fetch(f'/{dependency}/json')
latest_version = pypi_profile.get('info', {}).get('version', None)
print(f'{dependency} version {installed_version} (latest {latest_version})')


def _executables_checkup() -> None:
print('\n### External executables:\n')
for executable in [
'less',
'fzf',
'pygmentize',
]:
if shutil.which(executable):
print(f'The "{executable}" executable was found — good!')
else:
print(f'The recommended "{executable}" executable was not found — some functionality will suffer.')


def _environment_checkup() -> None:
print('\n### Environment variables:\n')
for variable in [
'EDITOR',
'VISUAL',
]:
if value := os.environ.get(variable):
print(f'The ${variable} environment variable was set to "{value}" — good!')
else:
print(f'The ${variable} environment variable was not set — some functionality will suffer.')


def _configuration_checkup(mycli) -> None:
did_output_missing = False
did_output_unsupported = False
did_output_deprecated = False

indent = ' '
transitions = {
f'{indent}[main]\n{indent}default_character_set': f'{indent}[connection]\n{indent}default_character_set',
f'{indent}[main]\n{indent}ssl_mode': f'{indent}[connection]\n{indent}default_ssl_mode',
}
reverse_transitions = {v: k for k, v in transitions.items()}

if not list(mycli.config.keys()):
print('\n### Missing file:\n')
print('The local ~/,myclirc is missing or empty.\n')
did_output_missing = True
else:
for section_name in mycli.config:
if section_name not in mycli.config_without_package_defaults:
if not did_output_missing:
print('\n### Missing in user ~/.myclirc:\n')
print(f'The entire section:\n\n{indent}[{section_name}]\n')
did_output_missing = True
continue
for item_name in mycli.config[section_name]:
transition_key = f'{indent}[{section_name}]\n{indent}{item_name}'
if transition_key in reverse_transitions:
continue
if item_name not in mycli.config_without_package_defaults[section_name]:
if not did_output_missing:
print('\n### Missing in user ~/.myclirc:\n')
print(f'The item:\n\n{indent}[{section_name}]\n{indent}{item_name} =\n')
did_output_missing = True

for section_name in mycli.config_without_package_defaults:
if section_name not in mycli.config_without_user_options:
if not did_output_unsupported:
print('\n### Unsupported in user ~/.myclirc:\n')
did_output_unsupported = True
print(f'The entire section:\n\n{indent}[{section_name}]\n')
continue
for item_name in mycli.config_without_package_defaults[section_name]:
if section_name == 'colors' and item_name.startswith('sql.'):
# these are commented out in the package myclirc
continue
if section_name in [
'favorite_queries',
'init-commands',
'alias_dsn',
'alias_dsn.init-commands',
]:
# these are free-entry sections, so a comparison per item is not meaningful
continue
transition_key = f'{indent}[{section_name}]\n{indent}{item_name}'
if transition_key in transitions:
continue
if item_name not in mycli.config_without_user_options[section_name]:
if not did_output_unsupported:
print('\n### Unsupported in user ~/.myclirc:\n')
print(f'The item:\n\n{indent}[{section_name}]\n{indent}{item_name} =\n')
did_output_unsupported = True

for section_name in mycli.config_without_package_defaults:
if section_name not in mycli.config_without_user_options:
continue
for item_name in mycli.config_without_package_defaults[section_name]:
if section_name == 'colors' and item_name.startswith('sql.'):
# these are commented out in the package myclirc
continue
transition_key = f'{indent}[{section_name}]\n{indent}{item_name}'
if transition_key in transitions:
if not did_output_deprecated:
print('\n### Deprecated in user ~/.myclirc:\n')
transition_value = transitions[transition_key]
print(f'It is recommended to transition:\n\n{transition_key}\n\nto\n\n{transition_value}\n')
did_output_deprecated = True

if did_output_missing or did_output_unsupported or did_output_deprecated:
print(f'For more info on supported features, see the commentary and defaults at:\n\n * {REPO_URL}/blob/main/mycli/myclirc\n')
else:
print('\n### Configuration:\n')
print('User configuration all up to date!\n')


def do_checkup(mycli) -> None:
_dependencies_checkup()
_executables_checkup()
_environment_checkup()
_configuration_checkup(mycli)
Loading