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

35 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-18 17:27 +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 if editor: 

46 # TODO? async 

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

48 ''' Executes editor with file. ''' 

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

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

51 return stream.read( ) 

52 return editor_executor 

53 _scribe.error( 

54 "No suitable text editor found. " 

55 "Please install a console-based editor " 

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

57 # TODO: Add suggestions. 

58 from .exceptions import ProgramAbsenceError 

59 raise ProgramAbsenceError( 'editor' ) 

60 

61 

62def edit_content( 

63 content: str = '', *, 

64 suffix: str = '.md', 

65 editor_discoverer: __.cabc.Callable[ 

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

67) -> str: 

68 ''' Edits content via discovered editor. ''' 

69 from .exceptions import EditorFailure, ProgramAbsenceError 

70 try: editor = editor_discoverer( ) 

71 except ProgramAbsenceError: return content 

72 import tempfile 

73 from pathlib import Path 

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

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

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

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

78 with tempfile.NamedTemporaryFile( 

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

80 ) as tmp: 

81 filename = tmp.name 

82 tmp.write( content ) 

83 try: return editor( filename ) 

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

85 finally: 

86 try: Path( filename ).unlink( ) 

87 except Exception: 

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