Coverage for sources/agentsmgr/cmdbase.py: 24%
50 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-22 02:08 +0000
« 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 -*-
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#============================================================================#
21''' Shared infrastructure for command implementations.
23 This module provides common utilities used across multiple command
24 implementations, including error handling, configuration management,
25 and data location resolution.
26'''
29import yaml as _yaml
31from . import __
32from . import core as _core
33from . import exceptions as _exceptions
34from . import sources as _sources
37CoderConfiguration: __.typx.TypeAlias = __.cabc.Mapping[ str, __.typx.Any ]
40def intercept_errors( ) -> __.cabc.Callable[
41 [ __.cabc.Callable[
42 ..., __.cabc.Coroutine[ __.typx.Any, __.typx.Any, None ] ] ],
43 __.cabc.Callable[
44 ..., __.cabc.Coroutine[ __.typx.Any, __.typx.Any, None ] ]
45]:
46 ''' Decorator for CLI command handlers to intercept and render errors.
48 Provides clean separation between business logic and error handling:
50 **Purpose**: Enables command implementations to focus purely on
51 business logic while the decorator handles all error presentation
52 concerns.
54 **Responsibilities**:
56 - Intercepts Omnierror exceptions from command execution
57 - Renders errors in appropriate format (markdown for CLI)
58 - Ensures proper exit code handling (SystemExit with code 1)
60 **Pattern**: Commands implement business logic and raise exceptions;
61 decorator handles presentation and process termination. This
62 separation ensures commands remain testable and focused.
64 **Type narrowing note**: The isinstance(auxdata, _core.Globals)
65 checks in individual command execute methods serve type narrowing
66 purposes and must be retained for proper type checking.
67 '''
68 def decorator(
69 function: __.cabc.Callable[
70 ..., __.cabc.Coroutine[ __.typx.Any, __.typx.Any, None ] ]
71 ) -> __.cabc.Callable[
72 ..., __.cabc.Coroutine[ __.typx.Any, __.typx.Any, None ]
73 ]:
74 @__.funct.wraps( function )
75 async def wrapper(
76 self: __.typx.Any,
77 auxdata: __.typx.Any,
78 *posargs: __.typx.Any,
79 **nomargs: __.typx.Any,
80 ) -> None:
81 try: return await function( self, auxdata, *posargs, **nomargs )
82 except _exceptions.Omnierror as exception:
83 if isinstance( auxdata, _core.Globals ):
84 await _core.render_and_print_result(
85 exception, auxdata.display, auxdata.exits )
86 else:
87 for line in exception.render_as_markdown( ):
88 print( line, file = __.sys.stderr )
89 raise SystemExit( 1 ) from None
90 return wrapper
91 return decorator
94async def retrieve_configuration(
95 target: __.Path,
96 profile: __.typx.Optional[ __.Path ] = None,
97) -> __.cabc.Mapping[ str, __.typx.Any ]:
98 ''' Loads and validates configuration from Copier answers file.
100 Unified configuration loading used by multiple command
101 implementations. Reads from standard Copier answers location
102 (or specified profile path) and validates required fields.
103 '''
104 if profile is not None:
105 answers_file = profile
106 else:
107 answers_file = (
108 target / ".auxiliary/configuration/copier-answers--agents.yaml" )
109 if not answers_file.exists( ):
110 raise _exceptions.ConfigurationAbsence( target )
111 try: content = answers_file.read_text( encoding = 'utf-8' )
112 except ( OSError, IOError ) as exception:
113 raise _exceptions.ConfigurationAbsence( ) from exception
114 try:
115 configuration: __.cabc.Mapping[ str, __.typx.Any ] = (
116 _yaml.safe_load( content ) )
117 except _yaml.YAMLError as exception:
118 raise _exceptions.ConfigurationInvalidity( exception ) from exception
119 if not isinstance( configuration, __.cabc.Mapping ):
120 raise _exceptions.ConfigurationInvalidity( )
121 await validate_configuration( configuration )
122 return configuration
125async def validate_configuration(
126 configuration: __.cabc.Mapping[ str, __.typx.Any ]
127) -> None:
128 ''' Validates required configuration fields are present and non-empty. '''
129 if not configuration.get( 'coders' ):
130 raise _exceptions.ConfigurationInvalidity( )
131 if not configuration.get( 'languages' ):
132 raise _exceptions.ConfigurationInvalidity( )
135def retrieve_data_location(
136 source_spec: str,
137 tag_prefix: __.typx.Annotated[
138 __.Absential[ str ],
139 __.ddoc.Doc(
140 "Prefix for filtering version tags when no explicit ref "
141 "is specified. Only tags starting with this prefix will be "
142 "considered, and the prefix will be stripped before version "
143 "parsing." ),
144 ] = __.absent,
145) -> __.Path:
146 ''' Resolves data source specification to local filesystem path.
148 Supports local paths, Git repositories, and remote sources through
149 pluggable source handlers. Uses registered handlers to resolve
150 various URL schemes to local filesystem paths.
151 '''
152 return _sources.resolve_source_location( source_spec, tag_prefix )
155def retrieve_variant_answers_file(
156 auxdata: _core.Globals, variant: str
157) -> __.Path:
158 ''' Retrieves path to variant answers file in test data directory.
160 Validates file existence and raises ConfigurationAbsence if not
161 found.
162 '''
163 data_directory = auxdata.provide_data_location( )
164 project_root = data_directory.parent
165 answers_file = (
166 project_root / 'tests' / 'data' / 'profiles'
167 / f"answers-{variant}.yaml" )
168 if not answers_file.exists( ):
169 raise _exceptions.ConfigurationAbsence( )
170 return answers_file