Coverage for sources/copiertv/cli.py: 64%
63 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-10 00:24 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-10 00:24 +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''' Command-line interface. '''
24from . import __
25from . import configuration as _configuration
26from . import engine as _engine
27from . import exceptions as _exceptions
28from . import state as _state
31def intercept_errors( ) -> __.cabc.Callable[
32 [ __.cabc.Callable[
33 ..., __.cabc.Coroutine[ __.typx.Any, __.typx.Any, None ] ] ],
34 __.cabc.Callable[
35 ..., __.cabc.Coroutine[ __.typx.Any, __.typx.Any, None ] ]
36]:
37 ''' Decorator that catches Omnierror for CLI display. '''
38 def decorator(
39 function: __.cabc.Callable[
40 ..., __.cabc.Coroutine[ __.typx.Any, __.typx.Any, None ] ]
41 ) -> __.cabc.Callable[
42 ..., __.cabc.Coroutine[ __.typx.Any, __.typx.Any, None ]
43 ]:
44 @__.functools.wraps( function )
45 async def wrapper(
46 *args: __.typx.Any, **kwargs: __.typx.Any
47 ) -> None:
48 try: await function( *args, **kwargs )
49 except _exceptions.Omnierror as exc:
50 renderer = getattr(
51 exc, 'render_as_markdown', None )
52 if renderer: 52 ↛ 55line 52 didn't jump to line 55 because the condition on line 52 was always true
53 lines = renderer( )
54 print( '\n'.join( lines ) )
55 raise SystemExit( 1 ) from exc
56 return wrapper
57 return decorator
60async def _survey( config: _configuration.Configuration ) -> None:
61 ''' Lists discovered template variants. '''
62 answers_dir = config.answers_directory
63 if __.is_absent( answers_dir ):
64 from .exceptions import ConfigurationInvalidity
65 raise ConfigurationInvalidity( 'answers directory' ) # noqa: TRY003
66 for variant in _engine.survey_variants( answers_dir ):
67 print( variant )
70async def _validate(
71 variant: str,
72 config: _configuration.Configuration,
73) -> None:
74 ''' Validates a template variant. '''
75 result = _engine.validate_variant( variant, config )
76 lines = result.render_as_markdown( )
77 print( '\n'.join( lines ) )
80class _SurveyCommand( __.appcore_cli.Command ):
81 ''' Surveys available template configuration variants. '''
83 @intercept_errors( )
84 async def execute( self, auxdata: __.Globals ) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
85 if not isinstance( auxdata, _state.Globals ):
86 raise _exceptions.ConfigurationInvalidity( )
87 await _survey( auxdata.copiertv_configuration )
90class _ValidateCommand( __.appcore_cli.Command ):
91 ''' Validates template against configuration variant. '''
93 variant: __.typx.Annotated[
94 str,
95 __.typx.Doc( ''' Configuration variant to validate. ''' ),
96 __.tyro.conf.Positional,
97 ]
98 preserve: __.typx.Annotated[
99 bool,
100 __.tyro.conf.arg(
101 help = 'Keep temporary files for inspection.',
102 prefix_name = False ),
103 ] = False
105 @intercept_errors( )
106 async def execute( self, auxdata: __.Globals ) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
107 if not isinstance( auxdata, _state.Globals ):
108 raise _exceptions.ConfigurationInvalidity( )
109 cli_config = _configuration.Configuration(
110 preserve = self.preserve )
111 config = _configuration.merge_configurations(
112 auxdata.copiertv_configuration, cli_config )
113 await _validate( self.variant, config )
116class _Application( __.appcore_cli.Application ):
117 ''' Validates Copier templates against configuration variants. '''
119 version: __.typx.Annotated[
120 bool,
121 __.tyro.conf.arg(
122 help = 'Display version and exit.',
123 prefix_name = False ),
124 ] = False
126 command: __.typx.Union[
127 __.typx.Annotated[
128 _SurveyCommand,
129 __.tyro.conf.subcommand(
130 'survey', prefix_name = False ),
131 ],
132 __.typx.Annotated[
133 _ValidateCommand,
134 __.tyro.conf.subcommand(
135 'validate', prefix_name = False ),
136 ],
137 ] = __.dcls.field( default_factory = _SurveyCommand )
139 async def execute( self, auxdata: __.Globals ) -> None:
140 ''' Dispatches to the selected command. '''
141 if self.version:
142 from . import __version__
143 print( f"copiertv {__version__}" )
144 raise SystemExit( 0 )
145 await self.command( auxdata )
147 async def prepare(
148 self, exits: __.ctxl.AsyncExitStack
149 ) -> _state.Globals:
150 ''' Prepares copiertv-specific global state. '''
151 auxdata_base = await super( ).prepare( exits )
152 config = _configuration.acquire_configuration(
153 auxdata_base.configuration )
154 return _state.Globals(
155 copiertv_configuration = config,
156 **{
157 field.name: getattr( auxdata_base, field.name )
158 for field in __.dcls.fields( auxdata_base )
159 if not field.name.startswith( '_' ) } )
162def execute( ) -> None:
163 ''' Entrypoint for CLI execution. '''
164 config = (
165 __.tyro.conf.EnumChoicesFromValues,
166 __.tyro.conf.HelptextFromCommentsOff,
167 )
168 try: __.asyncio.run( __.tyro.cli( _Application, config = config )( ) )
169 except SystemExit: raise
170 except BaseException: raise SystemExit( 1 ) from None