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

124 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-04 21:55 +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 CoderResourceAbsence( Omnierror, FileNotFoundError ): 

48 ''' Coder resource absence. ''' 

49 

50 def __init__( self, coder: str, path: __.Path ): 

51 message = f"Resources for {coder} not found at {path}" 

52 super( ).__init__( message ) 

53 

54 

55class CoderResourceCopyFailure( Omnierror, OSError ): 

56 ''' Coder resource copy failure. ''' 

57 

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

59 message = f"Failed to copy resources from {source} to {target}" 

60 super( ).__init__( message ) 

61 

62 

63class ConfigurationAbsence( Omnierror, FileNotFoundError ): 

64 

65 def __init__( 

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

67 ) -> None: 

68 message = "Could not locate agents configuration" 

69 if not __.is_absent( location ): 

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

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

72 

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

74 return ( 

75 f"❌ {self}", 

76 "", 

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

78 ) 

79 

80 

81class ConfigurationInvalidity( Omnierror, ValueError ): 

82 ''' Base configuration data invalidity. ''' 

83 

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

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

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

87 super( ).__init__( message ) 

88 

89 

90class ContentAbsence( Omnierror, FileNotFoundError ): 

91 ''' Content file absence. ''' 

92 

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

94 message = ( 

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

96 super( ).__init__( message ) 

97 

98 

99class FileOperationFailure( Omnierror, OSError ): 

100 ''' File or directory operation failure. ''' 

101 

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

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

104 super( ).__init__( message ) 

105 

106 

107class InstructionSourceInvalidity( Omnierror, ValueError ): 

108 ''' Instruction source configuration invalidity. ''' 

109 

110 

111class InstructionSourceFieldAbsence( InstructionSourceInvalidity ): 

112 ''' Instruction source 'source' field absence. ''' 

113 

114 def __init__( self ): 

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

116 super( ).__init__( message ) 

117 

118 

119class InstructionFilesConfigurationInvalidity( 

120 InstructionSourceInvalidity 

121): 

122 ''' Instruction files configuration format invalidity. ''' 

123 

124 def __init__( self ): 

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

126 super( ).__init__( message ) 

127 

128 

129class ContextInvalidity( Omnierror, TypeError ): 

130 ''' Invalid execution context. ''' 

131 

132 def __init__( self ): 

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

134 super( ).__init__( message ) 

135 

136 

137class DataSourceInvalidity( Omnierror, ValueError ): 

138 ''' Data source structure invalidity. ''' 

139 

140 def __init__( 

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

142 ) -> None: 

143 self.location = location 

144 self.missing_directories = missing_directories 

145 directories_list = ", ".join( missing_directories ) 

146 message = ( 

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

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

149 super( ).__init__( message ) 

150 

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

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

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

154 lines.append( "" ) 

155 lines.append( 

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

157 "directory structure:" ) 

158 lines.append( "" ) 

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

160 lines.append( "" ) 

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

162 lines.extend( 

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

164 lines.append( "" ) 

165 lines.append( 

166 "Data sources should contain structured directories for " 

167 "configurations, contents, and templates." ) 

168 return tuple( lines ) 

169 

170 

171class DataSourceNoSupport( Omnierror, ValueError ): 

172 ''' Unsupported data source format error. ''' 

173 

174 def __init__( self, source_spec: str ): 

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

176 super( ).__init__( message ) 

177 

178 

179class GlobalsPopulationFailure( Omnierror, OSError ): 

180 ''' Global settings population failure. ''' 

181 

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

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

184 super( ).__init__( message ) 

185 

186 

187class MemoryFileAbsence( Omnierror, FileNotFoundError ): 

188 ''' Memory file absence. 

189 

190 Raised when project memory file (AGENTS.md) does not exist 

191 but memory symlinks need to be created. 

192 ''' 

193 

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

195 self.location = location 

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

197 

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

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

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

201 lines.append( "" ) 

202 lines.append( 

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

204 "location:" ) 

205 lines.append( "" ) 

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

207 lines.append( "" ) 

208 lines.append( 

209 "Memory files provide project-specific conventions and " 

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

211 "running `agentsmgr populate`." ) 

212 lines.append( "" ) 

213 lines.append( 

214 "**Suggested action**: Create " 

215 "`.auxiliary/configuration/AGENTS.md` with " 

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

217 "project." ) 

218 return tuple( lines ) 

219 

220 

221class TargetModeNoSupport( Omnierror, ValueError ): 

222 ''' Targeting mode lack of support. ''' 

223 

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

225 self.coder = coder 

226 self.mode = mode 

227 self.reason = reason 

228 message = ( 

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

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

231 super( ).__init__( message ) 

232 

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

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

235 lines = [ 

236 "## Error: Unsupported Targeting Mode", 

237 "", 

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

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

240 ] 

241 if self.reason: 

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

243 return tuple( lines ) 

244 

245 

246class TemplateError( Omnierror, ValueError ): 

247 ''' Template processing error. ''' 

248 

249 def __init__( self, template_name: str ): 

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

251 

252 @classmethod 

253 def for_missing_template( 

254 cls, coder: str, item_type: str 

255 ) -> __.typx.Self: 

256 ''' Creates error for missing template. ''' 

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

258 

259 @classmethod 

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

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

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

263 

264 

265class ToolSpecificationInvalidity( ConfigurationInvalidity ): 

266 ''' Tool specification invalidity. ''' 

267 

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

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

270 super( ).__init__( message ) 

271 

272 

273class ToolSpecificationTypeInvalidity( ConfigurationInvalidity ): 

274 ''' Tool specification type invalidity. ''' 

275 

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

277 specification_type = type( specification ).__name__ 

278 message = ( 

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

280 f"{specification_type}" ) 

281 super( ).__init__( message )