Coverage for sources/appcore/introspection.py: 100%
73 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-27 22:40 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-27 22:40 +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''' Application for configuration introspection.
23 This module provides a complete CLI application for introspecting
24 configuration, environment variables, and platform directories. It serves
25 as both a practical utility and a comprehensive example of building CLI
26 applications with the :mod:`appcore.cli` framework.
28 Command-Line Interface
29 ======================
31 The ``appcore`` command provides three main introspection capabilities:
33 Configuration Inspection
34 ------------------------
35 Display finalized application configuration from TOML files::
37 appcore configuration # Default rich format
38 appcore --display.presentation json configuration
39 appcore --display.presentation toml configuration
41 Environment Variables
42 ---------------------
43 Show application-specific environment variables::
45 appcore environment # All APPCORE_* variables
46 appcore --display.presentation plain environment
48 Platform Directories
49 ---------------------
50 Display platform-specific directories for the application::
52 appcore directories # Show all directory paths
53 appcore --display.target-file dirs.txt directories
55 Presentation Formats
56 ====================
58 Multiple output formats are supported through the presentation option:
60 * ``rich`` (default) - Rich formatted output with syntax highlighting
61 * ``json`` - JSON format for programmatic consumption
62 * ``toml`` - TOML format matching input configuration files
63 * ``plain`` - Plain text format for simple displays
65 Output Routing
66 ==============
68 Flexible output destinations:
70 Stream Routing
71 --------------
72 * ``--display.target-stream stdout`` (default) - Main output to stdout
73 * ``--display.target-stream stderr`` - Main output to stderr
74 * ``--inscription.target-stream stderr`` (default) - Logging to stderr
75 * ``--inscription.target-stream stdout`` - Logging to stdout
77 File Output
78 -----------
79 * ``--display.target-file path`` - Save main output to file
80 * ``--inscription.target-file path`` - Save logging to file
82 Terminal Control
83 ================
85 Rich terminal behavior can be controlled:
87 * ``--display.colorize`` / ``--display.no-colorize`` - Control colorization
88 * ``--display.assume-rich-terminal`` - Force Rich capabilities (testing)
90 Implementation Architecture
91 ===========================
93 Command Structure
94 -----------------
95 * :class:`IntrospectConfigurationCommand` - Configuration introspection
96 * :class:`IntrospectEnvironmentCommand` - Environment variable inspection
97 * :class:`IntrospectDirectoriesCommand` - Platform directories inspection
98 * :class:`ApplicationGlobals` - Extended state for CLI context
100 Usage as Implementation Example
101 ================================
103 This module demonstrates comprehensive CLI application patterns:
105 * Command inheritance from :class:`appcore.cli.Command`
106 * Async execution with proper error handling
107 * Integration with appcore preparation and configuration systems
108 * File and stream output routing capabilities
109 * Rich terminal integration with automatic capability detection
111 The source code serves as a reference implementation for building similar
112 CLI applications with the appcore framework.
113'''
116import json as _json
118from . import __
119from . import cli as _cli
120from . import exceptions as _exceptions
121from . import state as _state
124try: import rich.console as _rich_console
125except ImportError as _error: # pragma: no cover
126 raise _exceptions.DependencyAbsence( 'rich', 'CLI' ) from _error
127try: import tomli_w as _tomli_w
128except ImportError as _error: # pragma: no cover
129 raise _exceptions.DependencyAbsence( 'tomli-w', 'CLI' ) from _error
130try: import tyro as _tyro
131except ImportError as _error: # pragma: no cover
132 raise _exceptions.DependencyAbsence( 'tyro', 'CLI' ) from _error
135class Presentations( __.enum.Enum ): # TODO: Python 3.11: StrEnum
136 ''' Presentation mode (format) for CLI output. '''
138 Json = 'json'
139 Plain = 'plain'
140 Rich = 'rich'
141 Toml = 'toml'
144class DisplayOptions( _cli.DisplayOptions ):
145 ''' Display options, including presentation mode. '''
147 presentation: __.typx.Annotated[
148 Presentations,
149 _tyro.conf.arg( help = "Output presentation mode (format)." ),
150 ] = Presentations.Rich
152 async def render( self, data: __.typx.Any ) -> None:
153 ''' Renders data according to display options. '''
154 async with __.ctxl.AsyncExitStack( ) as exits:
155 target = await self.provide_stream( exits )
156 match self.presentation:
157 case Presentations.Json:
158 content = _json.dumps(
159 data, indent = 2, ensure_ascii = False )
160 print( content, file = target )
161 case Presentations.Plain:
162 self._render_plain( data, target )
163 case Presentations.Rich:
164 if self.determine_colorization( target ):
165 self._render_rich( data, target)
166 else: self._render_plain( data, target )
167 case Presentations.Toml:
168 content = _tomli_w.dumps( data )
169 print( content, file = target )
171 def _render_plain(
172 self, data: __.typx.Any, target: __.typx.TextIO
173 ) -> None:
174 ''' Renders object in plain text format. '''
175 if isinstance( data, __.cabc.Mapping ):
176 for key, value in data.items( ): # pyright: ignore
177 print( f"{key}: {value}", file = target )
178 else: print( data, file = target ) # pragma: no cover
180 def _render_rich(
181 self, data: __.typx.Any, target: __.typx.TextIO
182 ) -> None:
183 ''' Renders object using Rich formatting. '''
184 console = _rich_console.Console(
185 file = target,
186 color_system = 'auto' if self.colorize else None )
187 console.print( data )
190class ApplicationGlobals( _state.Globals ):
191 ''' Includes display options. '''
193 display: DisplayOptions = __.dcls.field( default_factory = DisplayOptions )
196class IntrospectConfigurationCommand( _cli.Command ):
197 ''' Shows finalized application configuration. '''
199 async def execute( self, auxdata: _state.Globals ) -> None:
200 if not isinstance( auxdata, ApplicationGlobals ): # pragma: no cover
201 raise _exceptions.ContextInvalidity( auxdata )
202 data = dict( auxdata.configuration )
203 await auxdata.display.render( data )
206class IntrospectDirectoriesCommand( _cli.Command ):
207 ''' Shows application and package directories. '''
209 async def execute( self, auxdata: _state.Globals ):
210 if not isinstance( auxdata, ApplicationGlobals ): # pragma: no cover
211 raise _exceptions.ContextInvalidity( auxdata )
212 directories = {
213 'application-cache': str( auxdata.provide_cache_location( ) ),
214 'application-data': str( auxdata.provide_data_location( ) ),
215 'application-state': str( auxdata.provide_state_location( ) ),
216 'package-data': str(
217 auxdata.distribution.provide_data_location( )
218 ),
219 }
220 await auxdata.display.render( directories )
223class IntrospectEnvironmentCommand( _cli.Command ):
224 ''' Shows application-specific environment variables. '''
226 async def execute( self, auxdata: _state.Globals ) -> None:
227 if not isinstance( auxdata, ApplicationGlobals ): # pragma: no cover
228 raise _exceptions.ContextInvalidity( auxdata )
229 name = auxdata.application.name.upper( )
230 envvars = {
231 k: v for k, v in __.os.environ.items( )
232 if k.startswith( f"{name}_" ) }
233 await auxdata.display.render( envvars )
236class Application( _cli.Application ):
237 ''' Application for introspection of configuration. '''
239 display: DisplayOptions = __.dcls.field( default_factory = DisplayOptions )
240 command: __.typx.Union[
241 __.typx.Annotated[
242 IntrospectConfigurationCommand,
243 _tyro.conf.subcommand( 'configuration', prefix_name = False ),
244 ],
245 __.typx.Annotated[
246 IntrospectEnvironmentCommand,
247 _tyro.conf.subcommand( 'environment', prefix_name = False ),
248 ],
249 __.typx.Annotated[
250 IntrospectDirectoriesCommand,
251 _tyro.conf.subcommand( 'directories', prefix_name = False ),
252 ],
253 ] = __.dcls.field( default_factory = IntrospectConfigurationCommand )
255 async def execute( self, auxdata: _state.Globals ) -> None:
256 await self.command( auxdata )
258 async def prepare( self, exits: __.ctxl.AsyncExitStack ) -> _state.Globals:
259 auxdata_base = await super( ).prepare( exits )
260 nomargs = {
261 field.name: getattr( auxdata_base, field.name )
262 for field in __.dcls.fields( auxdata_base )
263 if not field.name.startswith( '_' ) }
264 return ApplicationGlobals( display = self.display, **nomargs )
267def execute_cli( ) -> None:
268 ''' Synchronous entrypoint. '''
269 if ( any( arg in ( '--help', '-h' ) for arg in __.sys.argv )
270 and _avoid_non_utf_terminals( )
271 ): return
272 configuration = ( _tyro.conf.EnumChoicesFromValues, )
273 __.asyncio.run( _tyro.cli( Application, config = configuration )( ) )
276def _avoid_non_utf_terminals( ) -> bool:
277 ''' Avoids terminals which do not support UTF charset encoding.
279 E.g., Git Bash Mintty terminals with cp1252 charset encoding.
280 '''
281 is_windows_cp1252 = (
282 __.sys.platform == 'win32'
283 and getattr( __.sys.stdout, 'encoding', '' ).lower( ) == 'cp1252' )
284 if not is_windows_cp1252: return False
285 encoding = getattr( __.sys.stdout, 'encoding', 'unknown' )
286 message = (
287 f"Help display is not available in this terminal "
288 f"(encoding: {encoding}).\n"
289 "Unicode characters required by the help system are not supported.\n\n"
290 "To view help, try running this command in:\n"
291 "- Windows Terminal\n"
292 "- PowerShell\n"
293 "- Command Prompt with UTF-8 support\n"
294 "- WSL\n\n"
295 "For basic usage: appcore <subcommand>\n"
296 "Available subcommands: configuration, environment, directories" )
297 print( message, file = __.sys.stderr )
298 raise SystemExit( 0 )