Coverage for sources/agentsmgr/exceptions.py: 37%

116 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-26 02:00 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Family of exceptions for package API. ''' 

22 

23 

24from . import __ 

25 

26 

27class Omniexception( __.immut.exceptions.Omniexception ): 

28 ''' Base for all exceptions raised by package API. ''' 

29 

30 

31class Omnierror( Omniexception, Exception ): 

32 ''' Base for error exceptions raised by package API. ''' 

33 

34 def render_as_markdown( self ) -> tuple[ str, ... ]: 

35 ''' Renders exception as Markdown lines for display. ''' 

36 return ( f"❌ {self}", ) 

37 

38 

39class CoderAbsence( Omnierror, ValueError ): 

40 ''' Coder absence in registry. ''' 

41 

42 def __init__( self, coder: str ): 

43 message = f"Coder not found in registry: {coder}" 

44 super( ).__init__( message ) 

45 

46 

47class ConfigurationAbsence( Omnierror, FileNotFoundError ): 

48 

49 def __init__( 

50 self, location: __.Absential[ __.Path ] = __.absent 

51 ) -> None: 

52 message = "Could not locate agents configuration" 

53 if not __.is_absent( location ): 

54 message = f"{message} at '{location}'" 

55 super( ).__init__( f"{message}." ) 

56 

57 def render_as_markdown( self ) -> tuple[ str, ... ]: 

58 return ( 

59 f"❌ {self}", 

60 "", 

61 "Run 'copier copy gh:emcd/agents-common' to configure agents." 

62 ) 

63 

64 

65class ConfigurationInvalidity( Omnierror, ValueError ): 

66 ''' Base configuration data invalidity. ''' 

67 

68 def __init__( self, reason: __.Absential[ str | Exception ] = __.absent ): 

69 if __.is_absent( reason ): message = "Invalid configuration." 

70 else: message = f"Invalid configuration: {reason}" 

71 super( ).__init__( message ) 

72 

73 

74 

75class ContentAbsence( Omnierror, FileNotFoundError ): 

76 ''' Content file absence. ''' 

77 

78 def __init__( self, content_type: str, content_name: str, coder: str ): 

79 message = ( 

80 f"No {content_type} content found for {coder}: {content_name}" ) 

81 super( ).__init__( message ) 

82 

83 

84class FileOperationFailure( Omnierror, OSError ): 

85 ''' File or directory operation failure. ''' 

86 

87 def __init__( self, path: __.Path, operation: str = "access file" ): 

88 message = f"Failed to {operation}: {path}" 

89 super( ).__init__( message ) 

90 

91 

92class InstructionSourceInvalidity( Omnierror, ValueError ): 

93 ''' Instruction source configuration invalidity. ''' 

94 

95 

96class InstructionSourceFieldAbsence( InstructionSourceInvalidity ): 

97 ''' Instruction source 'source' field absence. ''' 

98 

99 def __init__( self ): 

100 message = "Instruction source missing required 'source' field." 

101 super( ).__init__( message ) 

102 

103 

104class InstructionFilesConfigurationInvalidity( 

105 InstructionSourceInvalidity 

106): 

107 ''' Instruction files configuration format invalidity. ''' 

108 

109 def __init__( self ): 

110 message = "Instruction 'files' configuration must be a mapping." 

111 super( ).__init__( message ) 

112 

113 

114class ContextInvalidity( Omnierror, TypeError ): 

115 ''' Invalid execution context. ''' 

116 

117 def __init__( self ): 

118 message = "Invalid execution context: expected agentsmgr.cli.Globals" 

119 super( ).__init__( message ) 

120 

121 

122class DataSourceInvalidity( Omnierror, ValueError ): 

123 ''' Data source structure invalidity. ''' 

124 

125 def __init__( 

126 self, location: __.Path, missing_directories: tuple[ str, ... ] 

127 ) -> None: 

128 self.location = location 

129 self.missing_directories = missing_directories 

130 directories_list = ", ".join( missing_directories ) 

131 message = ( 

132 f"Invalid data source structure at {location}: " 

133 f"missing required directories: {directories_list}" ) 

134 super( ).__init__( message ) 

135 

136 def render_as_markdown( self ) -> tuple[ str, ... ]: 

137 ''' Renders data source invalidity with helpful guidance. ''' 

138 lines = [ "## Error: Invalid Data Source Structure" ] 

139 lines.append( "" ) 

140 lines.append( 

141 "The data source location does not contain the expected " 

142 "directory structure:" ) 

143 lines.append( "" ) 

144 lines.append( f" {self.location}" ) 

145 lines.append( "" ) 

146 lines.append( "**Missing required directories:**" ) 

147 lines.extend( 

148 f"- `{directory}`" for directory in self.missing_directories ) 

149 lines.append( "" ) 

150 lines.append( 

151 "Data sources should contain structured directories for " 

152 "configurations, contents, and templates." ) 

153 return tuple( lines ) 

154 

155 

156class DataSourceNoSupport( Omnierror, ValueError ): 

157 ''' Unsupported data source format error. ''' 

158 

159 def __init__( self, source_spec: str ): 

160 message = f"Unsupported source format: {source_spec}" 

161 super( ).__init__( message ) 

162 

163 

164 

165 

166class GlobalsPopulationFailure( Omnierror, OSError ): 

167 ''' Global settings population failure. ''' 

168 

169 def __init__( self, source: __.Path, target: __.Path ): 

170 message = f"Failed to populate global file from {source} to {target}" 

171 super( ).__init__( message ) 

172 

173 

174 

175class MemoryFileAbsence( Omnierror, FileNotFoundError ): 

176 ''' Memory file absence. 

177 

178 Raised when project memory file (conventions.md) does not exist 

179 but memory symlinks need to be created. 

180 ''' 

181 

182 def __init__( self, location: __.Path ) -> None: 

183 self.location = location 

184 super( ).__init__( f"Memory file not found: {location}" ) 

185 

186 def render_as_markdown( self ) -> tuple[ str, ... ]: 

187 ''' Renders memory file absence with helpful guidance. ''' 

188 lines = [ "## Error: Memory File Not Found" ] 

189 lines.append( "" ) 

190 lines.append( 

191 "The project memory file does not exist at the expected " 

192 "location:" ) 

193 lines.append( "" ) 

194 lines.append( f" {self.location}" ) 

195 lines.append( "" ) 

196 lines.append( 

197 "Memory files provide project-specific conventions and " 

198 "context to AI coding assistants. Create this file before " 

199 "running `agentsmgr populate`." ) 

200 lines.append( "" ) 

201 lines.append( 

202 "**Suggested action**: Create " 

203 "`.auxiliary/configuration/conventions.md` with " 

204 "project-specific conventions, or copy from a template " 

205 "project." ) 

206 return tuple( lines ) 

207 

208 

209class TargetModeNoSupport( Omnierror, ValueError ): 

210 ''' Targeting mode lack of support. ''' 

211 

212 def __init__( self, coder: str, mode: str, reason: str = '' ): 

213 self.coder = coder 

214 self.mode = mode 

215 self.reason = reason 

216 message = ( 

217 f"The {coder} coder does not support {mode} targeting mode." ) 

218 if reason: message = f"{message} {reason}" 

219 super( ).__init__( message ) 

220 

221 def render_as_markdown( self ) -> tuple[ str, ... ]: 

222 ''' Renders targeting mode error with helpful guidance. ''' 

223 lines = [ 

224 "## Error: Unsupported Targeting Mode", 

225 "", 

226 f"The **{self.coder}** coder does not support " 

227 f"**{self.mode}** targeting mode.", 

228 ] 

229 if self.reason: 

230 lines.extend( [ "", self.reason ] ) 

231 return tuple( lines ) 

232 

233 

234class TemplateError( Omnierror, ValueError ): 

235 ''' Template processing error. ''' 

236 

237 def __init__( self, template_name: str ): 

238 super( ).__init__( f"Template error: {template_name}" ) 

239 

240 @classmethod 

241 def for_missing_template( 

242 cls, coder: str, item_type: str 

243 ) -> __.typx.Self: 

244 ''' Creates error for missing template. ''' 

245 return cls( f"no {item_type} template found for {coder}" ) 

246 

247 @classmethod 

248 def for_extension_parse( cls, template_name: str ) -> __.typx.Self: 

249 ''' Creates error for extension parsing failure. ''' 

250 return cls( f"cannot determine output extension for {template_name}" ) 

251 

252 

253class ToolSpecificationInvalidity( ConfigurationInvalidity ): 

254 ''' Tool specification invalidity. ''' 

255 

256 def __init__( self, specification: __.typx.Any ): 

257 message = f"Unrecognized tool specification: {specification}" 

258 super( ).__init__( message ) 

259 

260 

261class ToolSpecificationTypeInvalidity( ConfigurationInvalidity ): 

262 ''' Tool specification type invalidity. ''' 

263 

264 def __init__( self, specification: __.typx.Any ): 

265 specification_type = type( specification ).__name__ 

266 message = ( 

267 f"Tool specification must be string or dict, got: " 

268 f"{specification_type}" ) 

269 super( ).__init__( message )