Coverage for sources / mimeogram / apply.py: 89%
85 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-18 17:27 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-18 17:27 +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 of mimeograms. '''
22# TODO? Use BSD sysexits.
25from . import __
26from . import exceptions as _exceptions
27from . import interfaces as _interfaces
28from . import parts as _parts
29from . import updaters as _updaters
32_scribe = __.produce_scribe( __name__ )
35class Command(
36 _interfaces.CliCommand,
37 decorators = ( __.standard_tyro_class, ),
38):
39 ''' Applies mimeogram to filesystem locations. '''
41 source: __.typx.Annotated[
42 str,
43 __.typx.Doc(
44 ''' Source file for mimeogram.
46 Defaults to stdin if '--clip' not specified.
47 ''' ),
48 ] = '-'
49 clip: __.typx.Annotated[
50 __.tyro.conf.DisallowNone[ bool | None ],
51 __.typx.Doc(
52 ''' Read mimeogram from clipboard instead of file or stdin. ''' ),
53 __.tyro.conf.arg( aliases = ( '--clipboard', '--from-clipboard' ) ),
54 ] = None
55 mode: __.typx.Annotated[
56 __.typx.Optional[ _updaters.ReviewModes ],
57 __.typx.Doc(
58 ''' Controls how changes are reviewed.
60 'silent': Apply without review.
61 'partitive': Review each change interactively.
63 Partitive, if not specified and on a terminal.
64 Silent, if not specified and not on a terminal.
65 ''' ),
66 __.tyro.conf.arg( aliases = ( '--review-mode', ) ),
67 ] = None
68 base: __.typx.Annotated[
69 __.typx.Optional[ __.Path ],
70 __.typx.Doc(
71 ''' Base directory for relative locations.
73 Defaults to current working directory.
74 ''' ),
75 __.tyro.conf.arg( aliases = ( '--base-directory', ) ),
76 ] = None
77 force: __.typx.Annotated[
78 __.tyro.conf.DisallowNone[ bool | None ],
79 __.typx.Doc( '''Override protected path checks.''' ),
80 ] = None
82 async def __call__(
83 self, auxdata: __.appcore.state.Globals
84 ) -> None:
85 ''' Executes command to apply mimeogram. '''
86 await apply( auxdata, self )
88 def provide_configuration_edits(
89 self,
90 ) -> __.appcore.dictedits.Edits:
91 ''' Provides edits against configuration from options. '''
92 edits: list[ __.appcore.dictedits.Edit ] = [ ]
93 if None is not self.clip:
94 edits.append( __.appcore.dictedits.SimpleEdit( # pyright: ignore
95 address = ( 'apply', 'from-clipboard' ), value = self.clip ) )
96 if None is not self.force:
97 edits.append( __.appcore.dictedits.SimpleEdit( # pyright: ignore
98 address = ( 'update-parts', 'disable-protections' ),
99 value = self.force ) )
100 return tuple( edits )
103class ContentAcquirer(
104 __.immut.DataclassProtocol, __.typx.Protocol,
105 decorators = ( __.typx.runtime_checkable, ),
106):
107 ''' Acquires content for apply command. '''
109 @__.abc.abstractmethod
110 def stdin_is_tty( self ) -> bool:
111 ''' Checks if input is from a terminal. '''
112 raise NotImplementedError
114 @__.abc.abstractmethod
115 async def acquire_clipboard( self ) -> str:
116 ''' Acquires content from clipboard. '''
117 raise NotImplementedError
119 @__.abc.abstractmethod
120 async def acquire_file( self, path: str | __.Path ) -> str:
121 ''' Acquires content from file. '''
122 raise NotImplementedError
124 @__.abc.abstractmethod
125 async def acquire_stdin( self ) -> str:
126 ''' Acquires content from standard input. '''
127 raise NotImplementedError
130class StandardContentAcquirer( ContentAcquirer ):
131 ''' Standard implementation of content acquisition. '''
133 def stdin_is_tty( self ) -> bool:
134 return __.sys.stdin.isatty( )
136 async def acquire_clipboard( self ) -> str:
137 from . import clipboard
138 return clipboard.copy_from_clipboard( )
140 async def acquire_file( self, path: str | __.Path ) -> str:
141 return await __.appcore.io.acquire_text_file_async( path )
143 async def acquire_stdin( self ) -> str:
144 return __.sys.stdin.read( )
147async def apply(
148 auxdata: __.appcore.state.Globals,
149 command: Command,
150 *,
151 acquirer: __.Absential[ ContentAcquirer ] = __.absent,
152 parser: __.Absential[
153 __.cabc.Callable[ [ str ], __.cabc.Sequence[ _parts.Part ] ]
154 ] = __.absent,
155 updater: __.Absential[
156 __.cabc.Callable[
157 [ __.appcore.state.Globals,
158 __.cabc.Sequence[ _parts.Part ],
159 _updaters.ReviewModes ],
160 __.cabc.Coroutine[ None, None, None ]
161 ]
162 ] = __.absent,
163) -> __.typx.Never:
164 ''' Applies mimeogram. '''
165 if __.is_absent( acquirer ): 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 acquirer = StandardContentAcquirer( )
167 if __.is_absent( parser ):
168 from .parsers import parse as parser
169 if __.is_absent( updater ):
170 from .updaters import update as updater
171 review_mode = _determine_review_mode( command, acquirer )
172 with _exceptions.report_exceptions(
173 _scribe, "Could not acquire mimeogram to apply."
174 ): mgtext = await _acquire( auxdata, command, acquirer )
175 if not mgtext:
176 _scribe.error( "Cannot apply empty mimeogram." )
177 raise SystemExit( 1 )
178 with _exceptions.report_exceptions(
179 _scribe, "Could not parse mimeogram."
180 ):
181 parts = parser( mgtext )
182 nomargs: dict[ str, __.typx.Any ] = { }
183 if command.base: nomargs[ 'base' ] = command.base
184 with _exceptions.report_exceptions(
185 _scribe, "Could not apply mimeogram."
186 ):
187 await updater( auxdata, parts, review_mode, **nomargs )
188 # TODO: If all parts ignored or inapplicable, then do not mention success.
189 _scribe.info( "Successfully applied mimeogram" )
190 raise SystemExit( 0 )
193async def _acquire(
194 auxdata: __.appcore.state.Globals,
195 cmd: Command,
196 acquirer: ContentAcquirer,
197) -> str:
198 ''' Acquires content to parse from clipboard, file, or stdin. '''
199 options = auxdata.configuration.get( 'apply', { } )
200 if options.get( 'from-clipboard', False ):
201 content = await acquirer.acquire_clipboard( )
202 if not content:
203 _scribe.error( "Clipboard is empty." )
204 raise SystemExit( 1 )
205 _scribe.debug(
206 "Read {} characters from clipboard.".format( len( content ) ) )
207 return content
208 match cmd.source:
209 case '-': return await acquirer.acquire_stdin( )
210 case _: return await acquirer.acquire_file( cmd.source )
213def _determine_review_mode(
214 command: Command, acquirer: ContentAcquirer
215) -> _updaters.ReviewModes:
216 on_tty = acquirer.stdin_is_tty( )
217 if command.mode is None:
218 if on_tty: return _updaters.ReviewModes.Partitive
219 return _updaters.ReviewModes.Silent
220 if not on_tty and command.mode is not _updaters.ReviewModes.Silent:
221 _scribe.error( "Cannot use an interactive mode without terminal." )
222 raise SystemExit( 1 )
223 return command.mode