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

88 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-22 02:08 +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 ContextInvalidity( Omnierror, TypeError ): 

93 ''' Invalid execution context. ''' 

94 

95 def __init__( self ): 

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

97 super( ).__init__( message ) 

98 

99 

100class DataSourceNoSupport( Omnierror, ValueError ): 

101 ''' Unsupported data source format error. ''' 

102 

103 def __init__( self, source_spec: str ): 

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

105 super( ).__init__( message ) 

106 

107 

108 

109 

110class GlobalsPopulationFailure( Omnierror, OSError ): 

111 ''' Global settings population failure. ''' 

112 

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

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

115 super( ).__init__( message ) 

116 

117 

118 

119class MemoryFileAbsence( Omnierror, FileNotFoundError ): 

120 ''' Memory file absence. 

121 

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

123 but memory symlinks need to be created. 

124 ''' 

125 

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

127 self.location = location 

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

129 

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

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

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

133 lines.append( "" ) 

134 lines.append( 

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

136 "location:" ) 

137 lines.append( "" ) 

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

139 lines.append( "" ) 

140 lines.append( 

141 "Memory files provide project-specific conventions and " 

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

143 "running `agentsmgr populate`." ) 

144 lines.append( "" ) 

145 lines.append( 

146 "**Suggested action**: Create " 

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

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

149 "project." ) 

150 return tuple( lines ) 

151 

152 

153class TargetModeNoSupport( Omnierror, ValueError ): 

154 ''' Targeting mode lack of support. ''' 

155 

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

157 self.coder = coder 

158 self.mode = mode 

159 self.reason = reason 

160 message = ( 

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

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

163 super( ).__init__( message ) 

164 

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

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

167 lines = [ 

168 "## Error: Unsupported Targeting Mode", 

169 "", 

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

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

172 ] 

173 if self.reason: 

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

175 return tuple( lines ) 

176 

177 

178class TemplateError( Omnierror, ValueError ): 

179 ''' Template processing error. ''' 

180 

181 def __init__( self, template_name: str ): 

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

183 

184 @classmethod 

185 def for_missing_template( 

186 cls, coder: str, item_type: str 

187 ) -> __.typx.Self: 

188 ''' Creates error for missing template. ''' 

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

190 

191 @classmethod 

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

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

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

195 

196 

197class ToolSpecificationInvalidity( ConfigurationInvalidity ): 

198 ''' Tool specification invalidity. ''' 

199 

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

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

202 super( ).__init__( message ) 

203 

204 

205class ToolSpecificationTypeInvalidity( ConfigurationInvalidity ): 

206 ''' Tool specification type invalidity. ''' 

207 

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

209 specification_type = type( specification ).__name__ 

210 message = ( 

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

212 f"{specification_type}" ) 

213 super( ).__init__( message )