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

36 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-02 23:41 +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 __future__ import annotations 

25 

26from . import __ 

27 

28 

29_scribe = __.produce_scribe( __name__ ) 

30 

31 

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

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

34 from shutil import which 

35 from subprocess import run # nosec B404 

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

37 for editor_ in filter( 

38 None, 

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

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

41 ): 

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

43 else: editor = '' 

44 match editor: 

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

46 case _: posargs = ( ) 

47 

48 if editor: 

49 

50 # TODO? async 

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

52 ''' Executes editor with file. ''' 

53 run( ( editor, *posargs, filename ), check = True ) # nosec B603 

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

55 return stream.read( ) 

56 

57 return editor_executor 

58 

59 _scribe.error( 

60 "No suitable text editor found. " 

61 "Please install a console-based editor " 

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

63 # TODO: Add suggestions. 

64 from .exceptions import ProgramAbsenceError 

65 raise ProgramAbsenceError( 'editor' ) 

66 

67 

68def edit_content( # pylint: disable=too-many-locals 

69 content: str = '', *, 

70 suffix: str = '.md', 

71 editor_discoverer: __.cabc.Callable[ 

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

73) -> str: 

74 ''' Edits content via discovered editor. ''' 

75 from .exceptions import EditorFailure, ProgramAbsenceError 

76 try: editor = editor_discoverer( ) 

77 except ProgramAbsenceError: return content 

78 import tempfile 

79 from pathlib import Path 

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

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

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

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

84 with tempfile.NamedTemporaryFile( 

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

86 ) as tmp: 

87 filename = tmp.name 

88 tmp.write( content ) 

89 try: return editor( filename ) # noqa: TRY101 

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

91 finally: 

92 try: Path( filename ).unlink( ) 

93 except Exception: # pylint: disable=broad-exception-caught 

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