44placed in this module and only imported as needed.
55"""
66import contextlib
7+ import io
78import math
89import os
910import sys
1314from ._compat import CYGWIN
1415from ._compat import get_best_encoding
1516from ._compat import isatty
16- from ._compat import open_stream
1717from ._compat import strip_ansi
1818from ._compat import term_len
1919from ._compat import WIN
@@ -329,62 +329,89 @@ def generator(self):
329329 self .render_progress ()
330330
331331
332- def pager (generator , color = None ):
333- """Decide what method to use for paging through text."""
332+ class StripAnsi (io .TextIOWrapper ):
333+ @classmethod
334+ def maybe (cls , stream , * , color , encoding ):
335+ if not getattr (stream , "encoding" , None ):
336+ if color :
337+ stream = io .TextIOWrapper (stream , encoding = encoding )
338+ else :
339+ stream = cls (stream )
340+ stream .color = color
341+ return stream
342+
343+ def write (self , text ):
344+ text = strip_ansi (text )
345+ return super ().write (text )
346+
347+
348+ @contextlib .contextmanager
349+ def get_pager_file (color = None ):
350+ """Context manager.
351+
352+ Yields a writable file-like object which can be used as an output pager.
353+
354+ .. versionadded:: 8.0
355+
356+ :param color: controls if the pager supports ANSI colors or not. The
357+ default is autodetection.
358+ """
334359 stdout = _default_text_stdout ()
335- if not isatty (sys .stdin ) or not isatty (stdout ):
336- return _nullpager (stdout , generator , color )
337360 pager_cmd = (os .environ .get ("PAGER" , None ) or "" ).strip ()
361+ env = dict (os .environ )
338362 if pager_cmd :
363+ # If we're piping to less we might support colors
364+ # if the right flags are passed...
365+ cmd_detail = pager_cmd .rsplit ("/" , 1 )[- 1 ].split ()
366+ if color is None and cmd_detail [0 ] == "less" :
367+ less_flags = f"{ os .environ .get ('LESS' , '' )} { ' ' .join (cmd_detail [1 :])} "
368+ if not less_flags :
369+ env ["LESS" ] = "-R"
370+ color = True
371+ elif "r" in less_flags or "R" in less_flags :
372+ color = True
373+ if not isatty (sys .stdin ) or not isatty (stdout ):
374+ ctx = contextlib .nullcontext ((stdout , None ))
375+ elif pager_cmd :
339376 if WIN :
340- return _tempfilepager (generator , pager_cmd , color )
341- return _pipepager (generator , pager_cmd , color )
342- if os .environ .get ("TERM" ) in ("dumb" , "emacs" ):
343- return _nullpager (stdout , generator , color )
344- if WIN or sys .platform .startswith ("os2" ):
345- return _tempfilepager (generator , "more <" , color )
346- if hasattr (os , "system" ) and os .system ("(less) 2>/dev/null" ) == 0 :
347- return _pipepager (generator , "less" , color )
377+ ctx = _tempfilepager (pager_cmd )
378+ else :
379+ ctx = _pipepager (pager_cmd , env = env )
380+ elif os .environ .get ("TERM" ) in ("dumb" , "emacs" ):
381+ ctx = contextlib .nullcontext ((stdout , None ))
382+ elif WIN or sys .platform .startswith ("os2" ):
383+ ctx = _tempfilepager ("more <" )
384+ elif hasattr (os , "system" ) and os .system ("(less) 2>/dev/null" ) == 0 :
385+ ctx = _pipepager ("less" , env = env )
386+ else :
387+ import tempfile
348388
349- import tempfile
389+ fd , filename = tempfile .mkstemp ()
390+ os .close (fd )
391+ try :
392+ if hasattr (os , "system" ) and os .system (f'more "{ filename } "' ) == 0 :
393+ ctx = _pipepager ("more" )
394+ else :
395+ ctx = contextlib .nullcontext ((stdout , None ))
396+ finally :
397+ os .unlink (filename )
350398
351- fd , filename = tempfile .mkstemp ()
352- os .close (fd )
353- try :
354- if hasattr (os , "system" ) and os .system (f'more "{ filename } "' ) == 0 :
355- return _pipepager (generator , "more" , color )
356- return _nullpager (stdout , generator , color )
357- finally :
358- os .unlink (filename )
399+ with ctx as (stream , encoding ):
400+ with StripAnsi .maybe (stream , color = color , encoding = encoding ) as text_stream :
401+ yield text_stream
359402
360403
361- def _pipepager (generator , cmd , color ):
404+ @contextlib .contextmanager
405+ def _pipepager (cmd , env = None ):
362406 """Page through text by feeding it to another program. Invoking a
363407 pager through this might support colors.
364408 """
365409 import subprocess
366410
367- env = dict (os .environ )
368-
369- # If we're piping to less we might support colors under the
370- # condition that
371- cmd_detail = cmd .rsplit ("/" , 1 )[- 1 ].split ()
372- if color is None and cmd_detail [0 ] == "less" :
373- less_flags = f"{ os .environ .get ('LESS' , '' )} { ' ' .join (cmd_detail [1 :])} "
374- if not less_flags :
375- env ["LESS" ] = "-R"
376- color = True
377- elif "r" in less_flags or "R" in less_flags :
378- color = True
379-
380411 c = subprocess .Popen (cmd , shell = True , stdin = subprocess .PIPE , env = env )
381- encoding = get_best_encoding (c .stdin )
382412 try :
383- for text in generator :
384- if not color :
385- text = strip_ansi (text )
386-
387- c .stdin .write (text .encode (encoding , "replace" ))
413+ encoding = get_best_encoding (c .stdin )
414+ yield c .stdin , encoding
388415 except (OSError , KeyboardInterrupt ):
389416 pass
390417 else :
@@ -407,30 +434,16 @@ def _pipepager(generator, cmd, color):
407434 break
408435
409436
410- def _tempfilepager (generator , cmd , color ):
437+ @contextlib .contextmanager
438+ def _tempfilepager (cmd ):
411439 """Page through text by invoking a program on a temporary file."""
412440 import tempfile
413441
414- filename = tempfile .mktemp ()
415- # TODO: This never terminates if the passed generator never terminates.
416- text = "" .join (generator )
417- if not color :
418- text = strip_ansi (text )
419442 encoding = get_best_encoding (sys .stdout )
420- with open_stream (filename , "wb" )[0 ] as f :
421- f .write (text .encode (encoding ))
422- try :
423- os .system (f'{ cmd } "{ filename } "' )
424- finally :
425- os .unlink (filename )
426-
427-
428- def _nullpager (stream , generator , color ):
429- """Simply print unformatted text. This is the ultimate fallback."""
430- for text in generator :
431- if not color :
432- text = strip_ansi (text )
433- stream .write (text )
443+ with tempfile .NamedTemporaryFile (mode = "wb" ) as f :
444+ yield f , encoding
445+ f .flush ()
446+ os .system (f'{ cmd } "{ f .name } "' )
434447
435448
436449class Editor :
0 commit comments