Coverage for sources/mimeogram/apply.py: 89%
85 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-29 23:11 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-29 23: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''' Application of mimeograms. '''
22# TODO? Use BSD sysexits.
25from __future__ import annotations
27from . import __
28from . import interfaces as _interfaces
29from . import parts as _parts
30from . import updaters as _updaters
33_scribe = __.produce_scribe( __name__ )
36class Command(
37 _interfaces.CliCommand,
38 decorators = ( __.standard_dataclass, __.standard_tyro_class ),
39):
40 ''' Applies mimeogram to filesystem locations. '''
42 source: __.typx.Annotated[
43 str,
44 __.typx.Doc(
45 ''' Source file for mimeogram.
47 Defaults to stdin if '--clip' not specified.
48 ''' ),
49 ] = '-'
50 clip: __.typx.Annotated[
51 __.typx.Optional[ bool ],
52 __.typx.Doc(
53 ''' Read mimeogram from clipboard instead of file or stdin. ''' ),
54 __.tyro.conf.arg( aliases = ( '--clipboard', '--from-clipboard' ) ),
55 ] = None
56 mode: __.typx.Annotated[
57 __.typx.Optional[ _updaters.ReviewModes ],
58 __.typx.Doc(
59 ''' Controls how changes are reviewed.
61 'silent': Apply without review.
62 'partitive': Review each change interactively.
64 Partitive, if not specified and on a terminal.
65 Silent, if not specified and not on a terminal.
66 ''' ),
67 __.tyro.conf.arg( aliases = ( '--review-mode', ) ),
68 ] = None
69 base: __.typx.Annotated[
70 __.typx.Optional[ __.Path ],
71 __.typx.Doc(
72 ''' Base directory for relative locations.
74 Defaults to current working directory.
75 ''' ),
76 __.tyro.conf.arg( aliases = ( '--base-directory', ) ),
77 ] = None
78 force: __.typx.Annotated[
79 __.typx.Optional[ bool ],
80 __.typx.Doc( '''Override protected path checks.''' ),
81 ] = None
83 async def __call__( self, auxdata: __.Globals ) -> None:
84 ''' Executes command to apply mimeogram. '''
85 await apply( auxdata, self )
87 def provide_configuration_edits( self ) -> __.DictionaryEdits:
88 ''' Provides edits against configuration from options. '''
89 edits: list[ __.DictionaryEdit ] = [ ]
90 if None is not self.clip:
91 edits.append( __.SimpleDictionaryEdit( # pyright: ignore
92 address = ( 'apply', 'from-clipboard' ), value = self.clip ) )
93 if None is not self.force:
94 edits.append( __.SimpleDictionaryEdit( # pyright: ignore
95 address = ( 'update-parts', 'disable-protections' ),
96 value = self.force ) )
97 return tuple( edits )
100class ContentAcquirer( # pylint: disable=invalid-metaclass
101 __.typx.Protocol,
102 metaclass = __.ImmutableStandardProtocolDataclass,
103 decorators = ( __.standard_dataclass, __.typx.runtime_checkable ),
104):
105 ''' Acquires content for apply command. '''
107 @__.abc.abstractmethod
108 def stdin_is_tty( self ) -> bool:
109 ''' Checks if input is from a terminal. '''
110 raise NotImplementedError
112 @__.abc.abstractmethod
113 async def acquire_clipboard( self ) -> str:
114 ''' Acquires content from clipboard. '''
115 raise NotImplementedError
117 @__.abc.abstractmethod
118 async def acquire_file( self, path: str | __.Path ) -> str:
119 ''' Acquires content from file. '''
120 raise NotImplementedError
122 @__.abc.abstractmethod
123 async def acquire_stdin( self ) -> str:
124 ''' Acquires content from standard input. '''
125 raise NotImplementedError
128class StandardContentAcquirer(
129 ContentAcquirer, decorators = ( __.standard_dataclass, )
130):
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 pyperclip import paste
138 return paste( )
140 async def acquire_file( self, path: str | __.Path ) -> str:
141 return await __.acquire_text_file_async( path )
143 async def acquire_stdin( self ) -> str:
144 return __.sys.stdin.read( )
147async def apply( # pylint: disable=too-complex
148 auxdata: __.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 [ __.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 __.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 __.report_exceptions( _scribe, "Could not parse mimeogram." ):
179 parts = parser( mgtext )
180 nomargs: dict[ str, __.typx.Any ] = { }
181 if command.base: nomargs[ 'base' ] = command.base
182 with __.report_exceptions( _scribe, "Could not apply mimeogram." ):
183 await updater( auxdata, parts, review_mode, **nomargs )
184 # TODO: If all parts ignored or inapplicable, then do not mention success.
185 _scribe.info( "Successfully applied mimeogram" )
186 raise SystemExit( 0 )
189async def _acquire(
190 auxdata: __.Globals, cmd: Command, acquirer: ContentAcquirer
191) -> str:
192 ''' Acquires content to parse from clipboard, file, or stdin. '''
193 options = auxdata.configuration.get( 'apply', { } )
194 if options.get( 'from-clipboard', False ):
195 content = await acquirer.acquire_clipboard( )
196 if not content:
197 _scribe.error( "Clipboard is empty." )
198 raise SystemExit( 1 )
199 _scribe.debug(
200 "Read {} characters from clipboard.".format( len( content ) ) )
201 return content
202 match cmd.source:
203 case '-': return await acquirer.acquire_stdin( )
204 case _: return await acquirer.acquire_file( cmd.source )
207def _determine_review_mode(
208 command: Command, acquirer: ContentAcquirer
209) -> _updaters.ReviewModes:
210 on_tty = acquirer.stdin_is_tty( )
211 if command.mode is None:
212 if on_tty: return _updaters.ReviewModes.Partitive
213 return _updaters.ReviewModes.Silent
214 if not on_tty and command.mode is not _updaters.ReviewModes.Silent:
215 _scribe.error( "Cannot use an interactive mode without terminal." )
216 raise SystemExit( 1 )
217 return command.mode