Coverage for src/irorun/cli.py: 44%
108 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-02 17:01 -0500
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-02 17:01 -0500
1# Copyright (c) 2025 Faith O. Oyedemi
2# SPDX-License-Identifier: Apache-2.0
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16# For more details, see the full text of the Apache License at:
17# http://www.apache.org/licenses/LICENSE-2.0
19import logging
20import os
21import subprocess
23import toml
24import typer
26from irorun.helpers import (
27 EnvManager,
28 create_poetry_project,
29 create_subdirectories,
30 create_uv_project,
31 create_virtualenv_project,
32)
33from irorun.logger_setup import setup_logging
35DEFAULT_PACKAGE_MANAGER: EnvManager = EnvManager.VIRTUALENV
37app = typer.Typer()
38logger = setup_logging()
41@app.callback()
42def main(
43 verbose: bool = typer.Option(
44 False, '--verbose', '-v', help='Enable verbose logging.'
45 ),
46):
47 """Global callback to adjust logging level based on the --verbose flag."""
48 if verbose:
49 logger.setLevel(logging.DEBUG)
50 logger.debug('Verbose logging enabled.')
51 else:
52 logger.setLevel(logging.DEBUG)
55def load_config(config_path: str = 'project_config.toml') -> dict:
56 if os.path.exists(config_path):
57 try:
58 config = toml.load(config_path)
59 logger.debug(f'Loaded configuration: {config}')
60 return config.get('init', {}) # Expecting a section [init]
61 except Exception as e:
62 logger.error(f'Error loading configuration: {e}')
63 typer.echo(f'Error reading configuration file: {e}')
64 else:
65 typer.echo(
66 f'Configuration file {config_path} not found. Either it does not exist or there is an issue with the file.'
67 )
68 typer.echo(
69 'Please ensure the file exists and is properly formatted. Or use `irorun gen-config` to generate one that you can edit.'
70 )
71 logger.warning(f'Configuration file {config_path} not found.')
72 return {}
75@app.command()
76def init(
77 project_dir: str = typer.Argument(
78 None, help='Project directory. If not provided, uses configuration value.'
79 ),
80 package_manager: EnvManager = typer.Option(
81 DEFAULT_PACKAGE_MANAGER,
82 '--package-manager',
83 help='Environment manager to use: poetry, uv, or virtualenv.',
84 ),
85):
86 """
87 Bootstraps a new project environment using settings from project_config.toml.
88 """
89 config_path = 'project_config.toml'
91 # Load configuration
92 typer.echo(f'Attempting to load configuration from {config_path}')
93 logger.info(f'Attempting to load configuration from {config_path}')
94 config = load_config()
95 logger.info(f'Back from loading configuration')
97 # Use configuration values (or defaults) if CLI arguments are not provided
98 project_dir = (
99 project_dir
100 if project_dir is not None
101 else config.get('project_directory', 'my_project')
102 )
104 config_package_manager = config.get('package_manager', 'uv')
105 package_manager = (
106 package_manager
107 if package_manager is not None
108 else EnvManager(config_package_manager)
109 )
111 # Create the project directory if it doesn't exist
112 venv_name = config.get('venv_name', 'venv')
113 dependencies = config.get('dependencies', [])
115 logging.success(f'Now checking if project directory exists')
116 try:
117 logger.debug(os.path.exists(project_dir))
118 if not os.path.exists(project_dir):
119 if package_manager == EnvManager.POETRY:
120 create_poetry_project(project_dir, dependencies)
121 elif package_manager == EnvManager.UV:
122 create_uv_project(project_dir, venv_name, dependencies)
123 elif package_manager == EnvManager.VIRTUALENV:
124 create_virtualenv_project(project_dir, venv_name, dependencies)
125 else:
126 typer.echo('Invalid package manager specified.', err=True)
127 raise typer.Exit(1)
128 except Exception as e:
129 raise typer.Exit(0) from e
130 typer.echo(f'Project directory already exists: {project_dir}')
131 # raise typer.Exit(1)
133 # Create subdirectories inside the project directory
134 subdirectories = config.get('subdirectories', [])
135 extra_subdirectories = config.get('extra_subdirectories', [])
136 all_subdirectories = subdirectories + extra_subdirectories
138 # Create subdirectories
139 if len(all_subdirectories) > 0:
140 typer.echo(f'Creating subdirectories: {all_subdirectories}')
141 create_subdirectories(project_dir, all_subdirectories)
142 typer.echo('Subdirectories created')
143 else:
144 typer.echo('No subdirectories specified')
147@app.command()
148def check():
149 """
150 Runs the code quality and formatting checks.
151 """
152 typer.echo('\nSorting imports...')
153 subprocess.run(['ruff check --select I --fix'], shell=True, check=False)
155 typer.echo('\nFormatting code...')
156 subprocess.run(['ruff format'], shell=True, check=False)
158 typer.echo('\nChecking code quality...')
159 subprocess.run(['ruff check'], shell=True, check=False)
160 typer.echo('Code quality completely checked')
161 typer.Exit(0)
164@app.command()
165def check_upgrade(
166 fix: bool = typer.Option(False, '--fix', help='Implement code syntax upgrade'),
167):
168 """
169 Checks your code syntax to see where it can be upgraded to meet the latest version.
170 """
171 typer.echo('Checking code syntax')
172 if fix:
173 subprocess.run(['ruff check --select UP --fix'], shell=True, check=False)
174 typer.echo('Code syntax upgraded')
175 else:
176 subprocess.run(['ruff check --select UP'], shell=True, check=False)
177 typer.echo('Code syntax checked')
180@app.command()
181def check_codestyle():
182 """
183 Checks your code style against some of the style conventions in PEP 8.
184 """
185 typer.echo('Checking code style')
186 subprocess.run(['ruff check --select E'], shell=True, check=False)
187 typer.echo('Code style checked')
190@app.command()
191def format():
192 """
193 Runs the formatting checks using ruff. This command does the following:
194 - Sorts imports and removes unused imports.
195 """
196 typer.echo('Formatting your code')
197 subprocess.run(['ruff format'], shell=True, check=False)
198 typer.echo('Formatting completed')
201@app.command()
202def gen_config():
203 """
204 Generates a configuration file for the project named project_config.toml.
205 """
206 typer.echo('Generating configuration file')
207 config_path = 'project_config.toml'
208 if os.path.exists(config_path):
209 typer.echo(f'Configuration file {config_path} already exists')
210 return
211 config = {
212 'init': {
213 'project_name': 'project_name',
214 'package_manager': 'virtualenv', # options - poetry, uv, or virtualenv
215 'venv_name': 'venv',
216 'subdirectories': ['src', 'docs', 'tests'],
217 'extra_subdirectories': [
218 'data',
219 'data/january',
220 'data/february',
221 'notebooks',
222 ],
223 'dependencies': ['numpy', 'pandas', 'matplotlib'],
224 }
225 }
226 with open(config_path, 'w') as f:
227 toml.dump(config, f)
228 typer.echo(f'Configuration file {config_path} created')
231@app.command()
232def document(
233 docs_dir=typer.Argument('docs', help='Directory for documentation files'),
234 author: str = typer.Option('Unset', help='Author name for documentation'),
235) -> None:
236 """Generates or updates the project documentation from documentation strings.
238 Notes
239 -----
240 Uses sphinx + reStructuredText and numpydoc.
241 """
242 raise NotImplementedError('Not implemented yet')
245if __name__ == '__main__':
246 logger.info('Starting the application')
247 app()