The Outerspace CLI is implemented using a modular command-based architecture. Each command is implemented as a subclass of BaseCommand, providing a consistent interface for command-line operations while allowing for command-specific functionality.
The main CLI entry point is defined in outerspace/cli/main.py. The Cli class handles:
- Argument parsing setup
- Command registration
- Command instantiation and execution
The main workflow is:
- Parse command-line arguments
- Initialize the appropriate command class
- Execute the command
The BaseCommand class (outerspace/cli/commands/base.py) serves as the foundation for all commands. It provides:
_init_parser(subparsers): Abstract method that each command must implement to define its command-line argumentsrun(): Abstract method that each command must implement to define its execution logic_load_config(config_file): Loads settings from a TOML configuration file_merge_config_and_args(defaults): Merges command-line arguments, config file settings, and defaults_chk_exists(filenames): Utility method to verify file existence
The BaseCommand class handles common functionality that should be consistent across all commands, while leaving command-specific logic to the subclasses:
-
Configuration Management
- Loading TOML configuration files
- Merging settings from different sources
- Tracking explicitly set arguments
- Providing default value handling
-
File Validation
- Checking file existence
- Validating file types (file vs directory)
- Providing consistent error messages
-
Command-Specific Arguments
- Defining command-specific argument parsers
- Setting up help text and argument groups
- Defining required vs optional arguments
-
Command Logic
- Implementing the actual command functionality
- Processing input files
- Generating output
- Handling command-specific errors
-
Command-Specific Validation
- Validating command-specific requirements
- Checking argument combinations
- Implementing command-specific file handling
The base command implements a sophisticated configuration system that merges settings from multiple sources. The merging process is handled by _merge_config_and_args() and follows a strict priority order:
-
Command-line Arguments (Highest Priority)
- Any argument explicitly set via command line takes precedence
- The system tracks which arguments were explicitly set by comparing against defaults
- Once an argument is marked as explicit, it cannot be overridden by config or defaults
-
Configuration File Settings (Medium Priority)
- Settings from the TOML config file are applied next
- Only applied to arguments that weren't explicitly set via command line
- Command-specific section is automatically detected based on class name
- Example:
FindSeqCommandlooks for[findseq]section
-
Default Values (Lowest Priority)
- Default values are applied to any remaining unset arguments
- These are typically defined in the command's
run()method - Serves as a fallback when no other value is specified
# In command subclass
def run(self):
# Define defaults
defaults = {
'input': None,
'output': 'output.txt',
'threads': 1
}
# Merge with config and args
self._merge_config_and_args(defaults)
# At this point, self.args will have values from:
# 1. Command line if explicitly set
# 2. Config file if not explicit
# 3. Defaults if neither of the above[findseq]
output = "default_output.txt"
threads = 4
[collapse]
input = "default_input.txt"In this example:
- If
--threads 8is specified on command line, it will use 8 - If not specified on command line but
threads = 4in config, it will use 4 - If neither is specified, it will use the default of 1
To implement a new command:
-
Create a new class that inherits from
BaseCommand -
Implement the required abstract methods:
def _init_parser(self, subparsers): parser = subparsers.add_parser('commandname', help='Description of command') # Add command-specific arguments return parser def run(self): # Implement command logic
-
Register the command in
main.py:- Add import statement
- Add to command list in
_init_parser() - Add to
command_mapin_init_command()
class ExampleCommand(BaseCommand):
def _init_parser(self, subparsers):
parser = subparsers.add_parser('example',
help='Example command')
parser.add_argument('--input',
help='Input file')
parser.add_argument('--output',
help='Output file')
return parser
def run(self):
# Load config if provided
if self.args.config:
self._load_config(self.args.config)
# Merge config and args with defaults
defaults = {
'input': None,
'output': None
}
self._merge_config_and_args(defaults)
# Implement command logicCommands can use TOML configuration files to provide default settings. The configuration system:
- Looks for a section matching the command name (lowercase, without 'Command' suffix)
- Merges these settings with command-line arguments
- Respects command-line argument precedence
The Cfg class in outerspace/config.py provides functionality to automatically generate TOML configuration files from the argument parser definitions. This ensures that configuration files stay in sync with the command-line interface. Key features:
-
Automatic Generation
- Creates TOML sections for each command
- Includes help text and type information as comments
- Preserves default values from argument definitions
- Handles both positional and optional arguments
-
Usage Example
from outerspace.config import Cfg # Generate default configuration cfg = Cfg("config.toml") cfg.write_file() # Creates config.toml with defaults
-
Generated TOML Format
[commandname] # Help text for argument1 # Type: str argument1 = "default_value" # Help text for argument2 # Type: int argument2 = 42
This automatic generation ensures that:
- Configuration files are always up-to-date with the CLI interface
- Help text and type information is preserved in the config file
- Default values are consistently applied
- The configuration format is standardized across all commands
Example TOML configuration:
[example]
input = "default_input.txt"
output = "default_output.txt"The base command provides built-in error handling for:
- Missing configuration files
- Non-existent files
- Invalid directories
- Missing required arguments
When implementing new commands:
- Always implement both required abstract methods
- Use the configuration system for default values
- Validate inputs using
_chk_exists()for file operations - Provide clear help text for all arguments
- Handle both single-file and batch processing cases when appropriate
- Use the configuration merging system to respect user preferences
- Keep command-specific logic in the subclass
- Leverage base class functionality for common operations
- Document any command-specific configuration options
- Use the built-in error handling where possible