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

97 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-24 01:49 +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 DataSourceNoSupport( Omnierror, ValueError ): 

123 ''' Unsupported data source format error. ''' 

124 

125 def __init__( self, source_spec: str ): 

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

127 super( ).__init__( message ) 

128 

129 

130 

131 

132class GlobalsPopulationFailure( Omnierror, OSError ): 

133 ''' Global settings population failure. ''' 

134 

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

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

137 super( ).__init__( message ) 

138 

139 

140 

141class MemoryFileAbsence( Omnierror, FileNotFoundError ): 

142 ''' Memory file absence. 

143 

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

145 but memory symlinks need to be created. 

146 ''' 

147 

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

149 self.location = location 

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

151 

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

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

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

155 lines.append( "" ) 

156 lines.append( 

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

158 "location:" ) 

159 lines.append( "" ) 

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

161 lines.append( "" ) 

162 lines.append( 

163 "Memory files provide project-specific conventions and " 

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

165 "running `agentsmgr populate`." ) 

166 lines.append( "" ) 

167 lines.append( 

168 "**Suggested action**: Create " 

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

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

171 "project." ) 

172 return tuple( lines ) 

173 

174 

175class TargetModeNoSupport( Omnierror, ValueError ): 

176 ''' Targeting mode lack of support. ''' 

177 

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

179 self.coder = coder 

180 self.mode = mode 

181 self.reason = reason 

182 message = ( 

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

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

185 super( ).__init__( message ) 

186 

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

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

189 lines = [ 

190 "## Error: Unsupported Targeting Mode", 

191 "", 

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

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

194 ] 

195 if self.reason: 

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

197 return tuple( lines ) 

198 

199 

200class TemplateError( Omnierror, ValueError ): 

201 ''' Template processing error. ''' 

202 

203 def __init__( self, template_name: str ): 

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

205 

206 @classmethod 

207 def for_missing_template( 

208 cls, coder: str, item_type: str 

209 ) -> __.typx.Self: 

210 ''' Creates error for missing template. ''' 

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

212 

213 @classmethod 

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

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

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

217 

218 

219class ToolSpecificationInvalidity( ConfigurationInvalidity ): 

220 ''' Tool specification invalidity. ''' 

221 

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

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

224 super( ).__init__( message ) 

225 

226 

227class ToolSpecificationTypeInvalidity( ConfigurationInvalidity ): 

228 ''' Tool specification type invalidity. ''' 

229 

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

231 specification_type = type( specification ).__name__ 

232 message = ( 

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

234 f"{specification_type}" ) 

235 super( ).__init__( message )