Source code for midi_diff.cli.main

"""
Author: 
    Inspyre Softworks

Project:
    MIDIDiff

File: 
    midi_diff/cli/main.py
 

Description:
    Main CLI logic and argument parsing for MIDIDiff.

"""
import argparse
import sys
from typing import Final, Sequence
from midi_diff.core import main as core_main
from midi_diff.cli.version import (
    print_version_info,
    print_debug_info,
    check_for_updates_command,
    upgrade_package,
    UPDATE_CHECK_ENV_VAR,
)
from midi_diff.cli.docs import open_documentation
from midi_diff.cli.completions import emit_completion_script, SUPPORTED_SHELLS, install_completions


# Subcommand names - single source of truth for CLI commands
# These are referenced by both build_parser() and backward compatibility logic
COMMAND_DIFF: Final[str] = 'diff'
COMMAND_DEBUG_INFO: Final[str] = 'debug-info'
COMMAND_CHECK_UPDATES: Final[str] = 'check-updates'
COMMAND_UPGRADE: Final[str] = 'upgrade'
COMMAND_DOCS: Final[str] = 'docs'
COMMAND_COMPLETION: Final[str] = 'completion'
COMMAND_INSTALL_COMPLETIONS: Final[str] = 'install-completions'

# Flag definitions - single source of truth for CLI flags
# These are referenced by both build_parser() and backward compatibility logic
FLAG_VERSION_SHORT: Final[str] = '-V'
FLAG_VERSION_LONG: Final[str] = '--version'
FLAG_HELP_SHORT: Final[str] = '-h'
FLAG_HELP_LONG: Final[str] = '--help'

# Known subcommands and flags for backward compatibility.
# These sets are derived from the constants above to ensure they stay
# synchronized with the parser configuration in build_parser().
KNOWN_COMMANDS: Final[frozenset[str]] = frozenset({COMMAND_DIFF, COMMAND_DEBUG_INFO, COMMAND_CHECK_UPDATES, COMMAND_UPGRADE, COMMAND_DOCS, COMMAND_COMPLETION, COMMAND_INSTALL_COMPLETIONS})
KNOWN_FLAGS: Final[frozenset[str]] = frozenset({FLAG_VERSION_SHORT, FLAG_VERSION_LONG, FLAG_HELP_SHORT, FLAG_HELP_LONG})
SUBCOMMAND_FLAGS: Final[dict[str, tuple[str, ...]]] = {
    COMMAND_DIFF: ("--help", "-h"),
    COMMAND_UPGRADE: ("--pre", "--help", "-h"),
    COMMAND_COMPLETION: ("--help", "-h"),
    COMMAND_INSTALL_COMPLETIONS: ("--shell", "--help", "-h"),
    COMMAND_DEBUG_INFO: ("--help", "-h"),
    COMMAND_CHECK_UPDATES: ("--help", "-h"),
    COMMAND_DOCS: ("--help", "-h"),
}


class VersionAction(argparse.Action):
    """Custom argparse action to print version info and exit."""
    
    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: str | Sequence[str] | None,
        option_string: str | None = None,
    ) -> None:
        print_version_info()
        parser.exit()


[docs] def build_parser() -> argparse.ArgumentParser: """ Build and return the argument parser for MIDIDiff CLI. Returns: Configured ArgumentParser instance """ parser = argparse.ArgumentParser( prog='midi-diff', description="MIDIDiff - Compare MIDI files and output their differences.", ) parser.add_argument( FLAG_VERSION_SHORT, FLAG_VERSION_LONG, action=VersionAction, nargs=0, help=( f"Show version and environment info " f"(set {UPDATE_CHECK_ENV_VAR} to a truthy value like '1', 'true', or 'yes' to check for updates)." ), ) # Create subparsers for subcommands subparsers = parser.add_subparsers( dest='command', help='Available commands', ) # diff subcommand (main functionality) diff_parser = subparsers.add_parser( COMMAND_DIFF, help='Compare two MIDI files and output their differences' ) diff_parser.add_argument("file_a", help="Path to the first MIDI file.") diff_parser.add_argument("file_b", help="Path to the second MIDI file.") diff_parser.add_argument("out_file", help="Path for the diff MIDI output.") # debug-info subcommand (no additional arguments needed) subparsers.add_parser( COMMAND_DEBUG_INFO, help='Display diagnostic and environment information' ) # check-updates subcommand (explicitly check for updates) subparsers.add_parser( COMMAND_CHECK_UPDATES, help='Check for available updates from PyPI' ) # upgrade subcommand (upgrade the package) upgrade_parser = subparsers.add_parser( COMMAND_UPGRADE, help='Upgrade midi-diff to the latest version from PyPI' ) upgrade_parser.add_argument( '--pre', action='store_true', help='Include pre-release versions in the upgrade' ) # docs subcommand (open documentation in browser) subparsers.add_parser( COMMAND_DOCS, help='Open MIDIDiff documentation in your default web browser' ) completion_parser = subparsers.add_parser( COMMAND_COMPLETION, help='Generate shell completion script', ) completion_parser.add_argument( "shell", choices=sorted(SUPPORTED_SHELLS), help="Shell to generate completion for (bash, zsh, fish, powershell, cmd)", ) install_completions_parser = subparsers.add_parser( COMMAND_INSTALL_COMPLETIONS, help='Install shell completions for the current shell', ) install_completions_parser.add_argument( "--shell", choices=sorted(SUPPORTED_SHELLS), help="Override detected shell", ) return parser
[docs] def run_cli(argv: Sequence[str] | None = None) -> None: """ Main CLI entry point for MIDIDiff. Parameters: argv: Command-line arguments to parse. If None, defaults to sys.argv[1:]. Accepts any sequence of strings (e.g., list, tuple) for testability. Usage: midi-diff fileA.mid fileB.mid output.mid (assumes 'diff' subcommand) midi-diff diff fileA.mid fileB.mid output.mid midi-diff debug-info midi-diff --version """ parser = build_parser() # Default to sys.argv[1:] if no argv provided if argv is None: argv = sys.argv[1:] # Combine known commands and flags for backward compatibility check known_subcommands_and_flags = KNOWN_COMMANDS | KNOWN_FLAGS # Backward compatibility: If first arg isn't a known subcommand/flag, # assume it's a file path and prepend 'diff' to make it work with the new structure. # Argparse will handle validation of the actual arguments. if len(argv) > 0 and argv[0] not in known_subcommands_and_flags: argv = [COMMAND_DIFF] + list(argv) args = parser.parse_args(argv) # Handle subcommands if args.command == COMMAND_DIFF: core_main(args.file_a, args.file_b, args.out_file) elif args.command == COMMAND_DEBUG_INFO: print_debug_info() elif args.command == COMMAND_CHECK_UPDATES: check_for_updates_command() elif args.command == COMMAND_UPGRADE: upgrade_package(include_pre=args.pre) elif args.command == COMMAND_DOCS: open_documentation() elif args.command == COMMAND_COMPLETION: script = emit_completion_script( args.shell, KNOWN_COMMANDS, KNOWN_FLAGS, SUBCOMMAND_FLAGS, ) print(script) elif args.command == COMMAND_INSTALL_COMPLETIONS: path = install_completions( args.shell, KNOWN_COMMANDS, KNOWN_FLAGS, SUBCOMMAND_FLAGS, ) print(f"Installed completions for '{path.name}' at: {path}") else: # No subcommand provided - show help parser.print_help() sys.exit(1)
__all__ = ["run_cli", "build_parser"]