Coverage for sources/mimeogram/edit.py: 35%

35 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''' System editor interaction. ''' 

22 

23 

24from . import __ 

25 

26 

27_scribe = __.produce_scribe( __name__ ) 

28 

29 

30def discover_editor( ) -> __.cabc.Callable[ [ str ], str ]: 

31 ''' Discovers editor and returns executor function. ''' 

32 from shutil import which 

33 from subprocess import run 

34 editor = __.os.environ.get( 'VISUAL' ) or __.os.environ.get( 'EDITOR' ) 

35 for editor_ in filter( 

36 None, 

37 # Editors, ranked by "friendliness", not by personal preference. 

38 ( editor, 'code', 'nano', 'emacs', 'nvim', 'vim' ) 

39 ): 

40 if ( editor := which( editor_ ) ): break 

41 else: editor = '' 

42 match editor: 

43 case 'code': posargs = ( '--wait', ) 

44 case _: posargs = ( ) 

45 

46 if editor: 

47 

48 # TODO? async 

49 def editor_executor( filename: str ) -> str: 

50 ''' Executes editor with file. ''' 

51 run( ( editor, *posargs, filename ), check = True ) # noqa: S603 

52 with open( filename, 'r', encoding = 'utf-8' ) as stream: 

53 return stream.read( ) 

54 

55 return editor_executor 

56 

57 _scribe.error( 

58 "No suitable text editor found. " 

59 "Please install a console-based editor " 

60 "or set the 'EDITOR' environment variable to your preferred editor." ) 

61 # TODO: Add suggestions. 

62 from .exceptions import ProgramAbsenceError 

63 raise ProgramAbsenceError( 'editor' ) 

64 

65 

66def edit_content( 

67 content: str = '', *, 

68 suffix: str = '.md', 

69 editor_discoverer: __.cabc.Callable[ 

70 [ ], __.cabc.Callable[ [ str ], str ] ] = discover_editor, 

71) -> str: 

72 ''' Edits content via discovered editor. ''' 

73 from .exceptions import EditorFailure, ProgramAbsenceError 

74 try: editor = editor_discoverer( ) 

75 except ProgramAbsenceError: return content 

76 import tempfile 

77 from pathlib import Path 

78 # Using delete = False to handle file cleanup manually. This ensures 

79 # the file handle is properly closed before the editor attempts to read it, 

80 # which is particularly important on Windows where open files cannot be 

81 # simultaneously accessed by other processes without a read share. 

82 with tempfile.NamedTemporaryFile( 

83 mode = 'w', suffix = suffix, delete = False, encoding = 'utf-8' 

84 ) as tmp: 

85 filename = tmp.name 

86 tmp.write( content ) 

87 try: return editor( filename ) 

88 except Exception as exc: raise EditorFailure( cause = exc ) from exc 

89 finally: 

90 try: Path( filename ).unlink( ) 

91 except Exception: 

92 _scribe.exception( f"Failed to cleanup {filename}" )