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
« 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 -*-
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''' System editor interaction. '''
24from . import __
27_scribe = __.produce_scribe( __name__ )
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' )
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}" )