Coverage for sources/copiertv/cli.py: 69%
58 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-14 02:11 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-14 02:11 +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 ''' Copiertv CLI application. '''
119 command: __.typx.Union[
120 __.typx.Annotated[
121 _SurveyCommand,
122 __.tyro.conf.subcommand(
123 'survey', prefix_name = False ),
124 ],
125 __.typx.Annotated[
126 _ValidateCommand,
127 __.tyro.conf.subcommand(
128 'validate', prefix_name = False ),
129 ],
130 ] = __.dcls.field( default_factory = _SurveyCommand )
132 async def execute( self, auxdata: __.Globals ) -> None:
133 ''' Dispatches to the selected command. '''
134 await self.command( auxdata )
136 async def prepare(
137 self, exits: __.ctxl.AsyncExitStack
138 ) -> _state.Globals:
139 ''' Prepares copiertv-specific global state. '''
140 auxdata_base = await super( ).prepare( exits )
141 config = _configuration.acquire_configuration(
142 auxdata_base.configuration )
143 return _state.Globals(
144 copiertv_configuration = config,
145 **{
146 field.name: getattr( auxdata_base, field.name )
147 for field in __.dcls.fields( auxdata_base )
148 if not field.name.startswith( '_' ) } )
151def execute( ) -> None:
152 ''' Entrypoint for CLI execution. '''
153 config = (
154 __.tyro.conf.EnumChoicesFromValues,
155 __.tyro.conf.HelptextFromCommentsOff,
156 )
157 try: __.asyncio.run( __.tyro.cli( _Application, config = config )( ) )
158 except SystemExit: raise
159 except BaseException: raise SystemExit( 1 ) from None