Source code for irorun.cli

# Copyright (c) 2025 Faith O. Oyedemi
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# For more details, see the full text of the Apache License at:
# http://www.apache.org/licenses/LICENSE-2.0

import logging
import os
import subprocess

import toml
import typer

from irorun.helpers import (
	EnvManager,
	create_poetry_project,
	create_subdirectories,
	create_uv_project,
	create_virtualenv_project,
)
from irorun.logger_setup import setup_logging

DEFAULT_PACKAGE_MANAGER: EnvManager = EnvManager.VIRTUALENV

app = typer.Typer()
logger = setup_logging()


[docs] @app.callback() def main( verbose: bool = typer.Option( False, '--verbose', '-v', help='Enable verbose logging.' ), ): """Global callback to adjust logging level based on the --verbose flag.""" if verbose: logger.setLevel(logging.DEBUG) logger.debug('Verbose logging enabled.') else: logger.setLevel(logging.DEBUG)
[docs] def load_config(config_path: str = 'project_config.toml') -> dict: if os.path.exists(config_path): try: config = toml.load(config_path) logger.debug(f'Loaded configuration: {config}') return config.get('init', {}) # Expecting a section [init] except Exception as e: logger.error(f'Error loading configuration: {e}') typer.echo(f'Error reading configuration file: {e}') else: typer.echo( f'Configuration file {config_path} not found. Either it does not exist or there is an issue with the file.' ) typer.echo( 'Please ensure the file exists and is properly formatted. Or use `irorun gen-config` to generate one that you can edit.' ) logger.warning(f'Configuration file {config_path} not found.') return {}
[docs] @app.command() def init( project_dir: str = typer.Argument( None, help='Project directory. If not provided, uses configuration value.' ), package_manager: EnvManager = typer.Option( DEFAULT_PACKAGE_MANAGER, '--package-manager', help='Environment manager to use: poetry, uv, or virtualenv.', ), ): """ Bootstraps a new project environment using settings from project_config.toml. """ config_path = 'project_config.toml' # Load configuration typer.echo(f'Attempting to load configuration from {config_path}') logger.info(f'Attempting to load configuration from {config_path}') config = load_config() logger.info(f'Back from loading configuration') # Use configuration values (or defaults) if CLI arguments are not provided project_dir = ( project_dir if project_dir is not None else config.get('project_directory', 'my_project') ) config_package_manager = config.get('package_manager', 'uv') package_manager = ( package_manager if package_manager is not None else EnvManager(config_package_manager) ) # Create the project directory if it doesn't exist venv_name = config.get('venv_name', 'venv') dependencies = config.get('dependencies', []) logging.info(f'Now checking if project directory exists') try: logger.debug(os.path.exists(project_dir)) if not os.path.exists(project_dir): if package_manager == EnvManager.POETRY: create_poetry_project(project_dir, dependencies) elif package_manager == EnvManager.UV: create_uv_project(project_dir, venv_name, dependencies) elif package_manager == EnvManager.VIRTUALENV: create_virtualenv_project(project_dir, venv_name, dependencies) else: typer.echo('Invalid package manager specified.', err=True) raise typer.Exit(1) except Exception as e: raise typer.Exit(0) from e typer.echo(f'Project directory already exists: {project_dir}') # raise typer.Exit(1) # Create subdirectories inside the project directory subdirectories = config.get('subdirectories', []) extra_subdirectories = config.get('extra_subdirectories', []) all_subdirectories = subdirectories + extra_subdirectories # Create subdirectories if len(all_subdirectories) > 0: typer.echo(f'Creating subdirectories: {all_subdirectories}') create_subdirectories(project_dir, all_subdirectories) typer.echo('Subdirectories created') else: typer.echo('No subdirectories specified')
[docs] @app.command() def check(): """ Runs the code quality and formatting checks. """ typer.echo('\nSorting imports...') subprocess.run(['ruff check --select I --fix'], shell=True, check=False) typer.echo('\nFormatting code...') subprocess.run(['ruff format'], shell=True, check=False) typer.echo('\nChecking code quality...') subprocess.run(['ruff check'], shell=True, check=False) typer.echo('Code quality completely checked') typer.Exit(0)
[docs] @app.command() def check_upgrade( fix: bool = typer.Option(False, '--fix', help='Implement code syntax upgrade'), ): """ Checks your code syntax to see where it can be upgraded to meet the latest version. """ typer.echo('Checking code syntax') if fix: subprocess.run(['ruff check --select UP --fix'], shell=True, check=False) typer.echo('Code syntax upgraded') else: subprocess.run(['ruff check --select UP'], shell=True, check=False) typer.echo('Code syntax checked')
[docs] @app.command() def check_codestyle(): """ Checks your code style against some of the style conventions in PEP 8. """ typer.echo('Checking code style') subprocess.run(['ruff check --select E'], shell=True, check=False) typer.echo('Code style checked')
[docs] @app.command() def format(): """ Runs the formatting checks using ruff. This command does the following: - Sorts imports and removes unused imports. """ typer.echo('Formatting your code') subprocess.run(['ruff format'], shell=True, check=False) typer.echo('Formatting completed')
[docs] @app.command() def gen_config(): """ Generates a configuration file for the project named project_config.toml. """ typer.echo('Generating configuration file') config_path = 'project_config.toml' if os.path.exists(config_path): typer.echo(f'Configuration file {config_path} already exists') return config = { 'init': { 'project_name': 'project_name', 'package_manager': 'virtualenv', # options - poetry, uv, or virtualenv 'venv_name': 'venv', 'subdirectories': ['src', 'docs', 'tests'], 'extra_subdirectories': [ 'data', 'data/january', 'data/february', 'notebooks', ], 'dependencies': ['numpy', 'pandas', 'matplotlib'], } } with open(config_path, 'w') as f: toml.dump(config, f) typer.echo(f'Configuration file {config_path} created')
[docs] @app.command() def document( docs_dir=typer.Argument('docs', help='Directory for documentation files'), author: str = typer.Option('Unset', help='Author name for documentation'), ) -> None: """ Generates or updates the project documentation from documentation strings. Notes ----- Uses sphinx + reStructuredText and numpydoc. """ raise NotImplementedError('Not implemented yet')
if __name__ == '__main__': logger.info('Starting the application') app()