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

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 

18 

19import logging 

20import os 

21import subprocess 

22 

23import toml 

24import typer 

25 

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 

34 

35DEFAULT_PACKAGE_MANAGER: EnvManager = EnvManager.VIRTUALENV 

36 

37app = typer.Typer() 

38logger = setup_logging() 

39 

40 

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) 

53 

54 

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 {} 

73 

74 

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' 

90 

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') 

96 

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 ) 

103 

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 ) 

110 

111 # Create the project directory if it doesn't exist 

112 venv_name = config.get('venv_name', 'venv') 

113 dependencies = config.get('dependencies', []) 

114 

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) 

132 

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 

137 

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') 

145 

146 

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) 

154 

155 typer.echo('\nFormatting code...') 

156 subprocess.run(['ruff format'], shell=True, check=False) 

157 

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) 

162 

163 

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') 

178 

179 

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') 

188 

189 

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') 

199 

200 

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') 

229 

230 

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. 

237 

238 Notes 

239 ----- 

240 Uses sphinx + reStructuredText and numpydoc. 

241 """ 

242 raise NotImplementedError('Not implemented yet') 

243 

244 

245if __name__ == '__main__': 

246 logger.info('Starting the application') 

247 app()