Coverage for sources/mimeogram/apply.py: 89%

84 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-05 19:15 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

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#============================================================================# 

19 

20 

21''' Application of mimeograms. ''' 

22# TODO? Use BSD sysexits. 

23 

24 

25from . import __ 

26from . import interfaces as _interfaces 

27from . import parts as _parts 

28from . import updaters as _updaters 

29 

30 

31_scribe = __.produce_scribe( __name__ ) 

32 

33 

34class Command( 

35 _interfaces.CliCommand, 

36 decorators = ( __.standard_tyro_class, ), 

37): 

38 ''' Applies mimeogram to filesystem locations. ''' 

39 

40 source: __.typx.Annotated[ 

41 str, 

42 __.typx.Doc( 

43 ''' Source file for mimeogram. 

44 

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. 

58 

59 'silent': Apply without review. 

60 'partitive': Review each change interactively. 

61 

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. 

71 

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 

80 

81 async def __call__( self, auxdata: __.Globals ) -> None: 

82 ''' Executes command to apply mimeogram. ''' 

83 await apply( auxdata, self ) 

84 

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 ) 

96 

97 

98class ContentAcquirer( 

99 __.immut.DataclassProtocol, __.typx.Protocol, 

100 decorators = ( __.typx.runtime_checkable, ), 

101): 

102 ''' Acquires content for apply command. ''' 

103 

104 @__.abc.abstractmethod 

105 def stdin_is_tty( self ) -> bool: 

106 ''' Checks if input is from a terminal. ''' 

107 raise NotImplementedError 

108 

109 @__.abc.abstractmethod 

110 async def acquire_clipboard( self ) -> str: 

111 ''' Acquires content from clipboard. ''' 

112 raise NotImplementedError 

113 

114 @__.abc.abstractmethod 

115 async def acquire_file( self, path: str | __.Path ) -> str: 

116 ''' Acquires content from file. ''' 

117 raise NotImplementedError 

118 

119 @__.abc.abstractmethod 

120 async def acquire_stdin( self ) -> str: 

121 ''' Acquires content from standard input. ''' 

122 raise NotImplementedError 

123 

124 

125class StandardContentAcquirer( ContentAcquirer ): 

126 ''' Standard implementation of content acquisition. ''' 

127 

128 def stdin_is_tty( self ) -> bool: 

129 return __.sys.stdin.isatty( ) 

130 

131 async def acquire_clipboard( self ) -> str: 

132 from pyperclip import paste 

133 return paste( ) 

134 

135 async def acquire_file( self, path: str | __.Path ) -> str: 

136 return await __.acquire_text_file_async( path ) 

137 

138 async def acquire_stdin( self ) -> str: 

139 return __.sys.stdin.read( ) 

140 

141 

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 ) 

182 

183 

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 ) 

200 

201 

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