Coverage for src/irorun/cli.py: 44%

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

19 import logging

20 import os

21 import subprocess

22

23 import toml

24 import typer

25

26 from irorun.helpers import (

27 EnvManager,

28 create_poetry_project,

29 create_subdirectories,

30 create_uv_project,

31 create_virtualenv_project,

32)

33 from irorun.logger_setup import setup_logging

34

35 DEFAULT_PACKAGE_MANAGER: EnvManager = EnvManager.VIRTUALENV

36

37 app = typer.Typer()

38 logger = setup_logging()

39

40

41@app.callback()

42 def 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

55 def 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()

76 def 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()

148 def 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()

165 def 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()

181 def 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()

191 def 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()

202 def 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()

232 def 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

245 if __name__ == '__main__':

246 logger.info('Starting the application')

247 app()