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