Coverage for sources/mimeogram/apply.py: 89%
85 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-22 03:00 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-22 03:00 +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, # TODO: str | Path
44 __.tyro.conf.arg(
45 help = (
46 "Source file for mimeogram. "
47 "Defaults to stdin if '--clip' not specified." ) ),
48 ] = '-'
49 clip: __.typx.Annotated[
50 __.typx.Optional[ bool ],
51 __.tyro.conf.arg(
52 aliases = ( '--clipboard', '--from-clipboard' ),
53 help = "Read mimeogram from clipboard instead of file or stdin." ),
54 ] = None
55 mode: __.typx.Annotated[
56 __.typx.Optional[ _updaters.ReviewModes ],
57 __.tyro.conf.arg(
58 aliases = ( '--review-mode', ),
59 help = (
60 "Controls how changes are reviewed. "
61 "'silent': Apply without review. "
62 "'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 ] = None
66 base: __.typx.Annotated[
67 __.typx.Optional[ __.Path ],
68 __.tyro.conf.arg(
69 aliases = ( '--base-directory', ),
70 help = (
71 "Base directory for relative locations. "
72 "Defaults to current working directory." ) ),
73 ] = None
74 force: __.typx.Annotated[
75 __.typx.Optional[ bool ],
76 __.tyro.conf.arg(
77 help = 'Override protected path checks.' ),
78 ] = None
80 async def __call__( self, auxdata: __.Globals ) -> None:
81 ''' Executes command to apply mimeogram. '''
82 await apply( auxdata, self )
84 def provide_configuration_edits( self ) -> __.DictionaryEdits:
85 ''' Provides edits against configuration from options. '''
86 edits: list[ __.DictionaryEdit ] = [ ]
87 if None is not self.clip:
88 edits.append( __.SimpleDictionaryEdit( # pyright: ignore
89 address = ( 'apply', 'from-clipboard' ), value = self.clip ) )
90 if None is not self.force:
91 edits.append( __.SimpleDictionaryEdit( # pyright: ignore
92 address = ( 'update-parts', 'disable-protections' ),
93 value = self.force ) )
94 return tuple( edits )
97class ContentAcquirer( # pylint: disable=invalid-metaclass
98 __.typx.Protocol,
99 metaclass = __.ImmutableStandardProtocolDataclass,
100 decorators = ( __.standard_dataclass, __.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(
126 ContentAcquirer, decorators = ( __.standard_dataclass, )
127):
128 ''' Standard implementation of content acquisition. '''
130 def stdin_is_tty( self ) -> bool:
131 return __.sys.stdin.isatty( )
133 async def acquire_clipboard( self ) -> str:
134 from pyperclip import paste
135 return paste( )
137 async def acquire_file( self, path: str | __.Path ) -> str:
138 return await __.acquire_text_file_async( path )
140 async def acquire_stdin( self ) -> str:
141 return __.sys.stdin.read( )
144async def apply( # pylint: disable=too-complex
145 auxdata: __.Globals,
146 command: Command,
147 *,
148 acquirer: __.Absential[ ContentAcquirer ] = __.absent,
149 parser: __.Absential[
150 __.cabc.Callable[ [ str ], __.cabc.Sequence[ _parts.Part ] ]
151 ] = __.absent,
152 updater: __.Absential[
153 __.cabc.Callable[
154 [ __.Globals,
155 __.cabc.Sequence[ _parts.Part ],
156 _updaters.ReviewModes ],
157 __.cabc.Coroutine[ None, None, None ]
158 ]
159 ] = __.absent,
160) -> __.typx.Never:
161 ''' Applies mimeogram. '''
162 if __.is_absent( acquirer ): 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true
163 acquirer = StandardContentAcquirer( )
164 if __.is_absent( parser ):
165 from .parsers import parse as parser
166 if __.is_absent( updater ):
167 from .updaters import update as updater
168 review_mode = _determine_review_mode( command, acquirer )
169 with __.report_exceptions(
170 _scribe, "Could not acquire mimeogram to apply."
171 ): mgtext = await _acquire( auxdata, command, acquirer )
172 if not mgtext:
173 _scribe.error( "Cannot apply empty mimeogram." )
174 raise SystemExit( 1 )
175 with __.report_exceptions( _scribe, "Could not parse mimeogram." ):
176 parts = parser( mgtext )
177 nomargs: dict[ str, __.typx.Any ] = { }
178 if command.base: nomargs[ 'base' ] = command.base
179 with __.report_exceptions( _scribe, "Could not apply mimeogram." ):
180 await updater( auxdata, parts, review_mode, **nomargs )
181 # TODO: If all parts ignored or inapplicable, then do not mention success.
182 _scribe.info( "Successfully applied mimeogram" )
183 raise SystemExit( 0 )
186async def _acquire(
187 auxdata: __.Globals, cmd: Command, acquirer: ContentAcquirer
188) -> str:
189 ''' Acquires content to parse from clipboard, file, or stdin. '''
190 options = auxdata.configuration.get( 'apply', { } )
191 if options.get( 'from-clipboard', False ):
192 content = await acquirer.acquire_clipboard( )
193 if not content:
194 _scribe.error( "Clipboard is empty." )
195 raise SystemExit( 1 )
196 _scribe.debug(
197 "Read {} characters from clipboard.".format( len( content ) ) )
198 return content
199 match cmd.source:
200 case '-': return await acquirer.acquire_stdin( )
201 case _: return await acquirer.acquire_file( cmd.source )
204def _determine_review_mode(
205 command: Command, acquirer: ContentAcquirer
206) -> _updaters.ReviewModes:
207 on_tty = acquirer.stdin_is_tty( )
208 if command.mode is None:
209 if on_tty: return _updaters.ReviewModes.Partitive
210 return _updaters.ReviewModes.Silent
211 if not on_tty and command.mode is not _updaters.ReviewModes.Silent:
212 _scribe.error( "Cannot use an interactive mode without terminal." )
213 raise SystemExit( 1 )
214 return command.mode