# 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 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()