88
99import collections .abc as cabc
1010import contextlib
11+ import io
1112import math
1213import os
1314import shlex
2324from ._compat import CYGWIN
2425from ._compat import get_best_encoding
2526from ._compat import isatty
26- from ._compat import open_stream
2727from ._compat import strip_ansi
2828from ._compat import term_len
2929from ._compat import WIN
@@ -366,7 +366,20 @@ def generator(self) -> cabc.Iterator[V]:
366366 self .render_progress ()
367367
368368
369- def pager (generator : cabc .Iterable [str ], color : bool | None = None ) -> None :
369+ class MaybeStripAnsi (io .TextIOWrapper ):
370+ def __init__ (self , stream : t .IO [bytes ], * , color : bool , ** kwargs : t .Any ):
371+ super ().__init__ (stream , ** kwargs )
372+ self .color = color
373+
374+ def write (self , text : str ) -> int :
375+ if not self .color :
376+ text = strip_ansi (text )
377+ return super ().write (text )
378+
379+
380+ def _pager_contextmanager (
381+ color : bool | None = None ,
382+ ) -> t .ContextManager [t .Tuple [t .BinaryIO , str , bool ]]:
370383 """Decide what method to use for paging through text."""
371384 stdout = _default_text_stdout ()
372385
@@ -376,50 +389,52 @@ def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None:
376389 stdout = StringIO ()
377390
378391 if not isatty (sys .stdin ) or not isatty (stdout ):
379- return _nullpager (stdout , generator , color )
392+ return _nullpager (stdout , color )
380393
381394 # Split and normalize the pager command into parts.
382395 pager_cmd_parts = shlex .split (os .environ .get ("PAGER" , "" ), posix = False )
383396 if pager_cmd_parts :
384397 if WIN :
385- if _tempfilepager (generator , pager_cmd_parts , color ):
386- return
387- elif _pipepager (generator , pager_cmd_parts , color ):
388- return
398+ return _tempfilepager (pager_cmd_parts , color )
399+ return _pipepager (pager_cmd_parts , color )
389400
390401 if os .environ .get ("TERM" ) in ("dumb" , "emacs" ):
391- return _nullpager (stdout , generator , color )
392- if (WIN or sys .platform .startswith ("os2" )) and _tempfilepager (
393- generator , ["more" ], color
394- ):
395- return
396- if _pipepager (generator , ["less" ], color ):
397- return
398-
399- import tempfile
400-
401- fd , filename = tempfile .mkstemp ()
402- os .close (fd )
403- try :
404- if _pipepager (generator , ["more" ], color ):
405- return
406- return _nullpager (stdout , generator , color )
407- finally :
408- os .unlink (filename )
402+ return _nullpager (stdout , color )
403+ if WIN or sys .platform .startswith ("os2" ):
404+ return _tempfilepager (["more" ], color )
405+ return _pipepager (["less" ], color )
406+
407+
408+ @contextlib .contextmanager
409+ def get_pager_file (color : bool | None = None ) -> t .Generator [t .IO , None , None ]:
410+ """Context manager.
411+ Yields a writable file-like object which can be used as an output pager.
412+ .. versionadded:: 8.2
413+ :param color: controls if the pager supports ANSI colors or not. The
414+ default is autodetection.
415+ """
416+ with _pager_contextmanager (color = color ) as (stream , encoding , color ):
417+ if not getattr (stream , "encoding" , None ):
418+ # wrap in a text stream
419+ stream = MaybeStripAnsi (stream , color = color , encoding = encoding )
420+ yield stream
421+ stream .flush ()
409422
410423
424+ @contextlib .contextmanager
411425def _pipepager (
412- generator : cabc . Iterable [ str ], cmd_parts : list [str ], color : bool | None
413- ) -> bool :
426+ cmd_parts : list [str ], color : bool | None = None
427+ ) -> t . Iterator [ t . Tuple [ t . BinaryIO , str , bool ]] :
414428 """Page through text by feeding it to another program. Invoking a
415429 pager through this might support colors.
416-
417- Returns `True` if the command was found, `False` otherwise and thus another
418- pager should be attempted.
419430 """
420431 # Split the command into the invoked CLI and its parameters.
421432 if not cmd_parts :
422- return False
433+ # Return a no-op context manager that yields None
434+ @contextlib .contextmanager
435+ def _noop ():
436+ yield None , "" , False
437+ return _noop ()
423438
424439 import shutil
425440
@@ -428,7 +443,11 @@ def _pipepager(
428443
429444 cmd_filepath = shutil .which (cmd )
430445 if not cmd_filepath :
431- return False
446+ # Return a no-op context manager
447+ @contextlib .contextmanager
448+ def _noop ():
449+ yield None , "" , False
450+ return _noop ()
432451
433452 # Produces a normalized absolute path string.
434453 # multi-call binaries such as busybox derive their identity from the symlink
@@ -451,6 +470,9 @@ def _pipepager(
451470 elif "r" in less_flags or "R" in less_flags :
452471 color = True
453472
473+ if color is None :
474+ color = False
475+
454476 c = subprocess .Popen (
455477 [str (cmd_path )] + cmd_params ,
456478 shell = False ,
@@ -459,13 +481,10 @@ def _pipepager(
459481 errors = "replace" ,
460482 text = True ,
461483 )
462- assert c .stdin is not None
484+ stdin = t .cast (t .BinaryIO , c .stdin )
485+ encoding = get_best_encoding (stdin )
463486 try :
464- for text in generator :
465- if not color :
466- text = strip_ansi (text )
467-
468- c .stdin .write (text )
487+ yield stdin , encoding , color
469488 except BrokenPipeError :
470489 # In case the pager exited unexpectedly, ignore the broken pipe error.
471490 pass
@@ -479,7 +498,7 @@ def _pipepager(
479498 finally :
480499 # We must close stdin and wait for the pager to exit before we continue
481500 try :
482- c . stdin .close ()
501+ stdin .close ()
483502 # Close implies flush, so it might throw a BrokenPipeError if the pager
484503 # process exited already.
485504 except BrokenPipeError :
@@ -501,64 +520,54 @@ def _pipepager(
501520 else :
502521 break
503522
504- return True
505-
506523
524+ @contextlib .contextmanager
507525def _tempfilepager (
508- generator : cabc .Iterable [str ], cmd_parts : list [str ], color : bool | None
509- ) -> bool :
510- """Page through text by invoking a program on a temporary file.
511-
512- Returns `True` if the command was found, `False` otherwise and thus another
513- pager should be attempted.
514- """
526+ cmd_parts : list [str ], color : bool | None = None
527+ ) -> t .Iterator [t .Tuple [t .BinaryIO , str , bool ]]:
528+ """Page through text by invoking a program on a temporary file."""
515529 # Split the command into the invoked CLI and its parameters.
516530 if not cmd_parts :
517- return False
531+ # Return a no-op context manager
532+ @contextlib .contextmanager
533+ def _noop ():
534+ yield None , "" , False
535+ return _noop ()
518536
519537 import shutil
538+ import subprocess
520539
521540 cmd = cmd_parts [0 ]
522541
523542 cmd_filepath = shutil .which (cmd )
524543 if not cmd_filepath :
525- return False
544+ # Return a no-op context manager
545+ @contextlib .contextmanager
546+ def _noop ():
547+ yield None , "" , False
548+ return _noop ()
549+
526550 # Produces a normalized absolute path string.
527551 # multi-call binaries such as busybox derive their identity from the symlink
528552 # less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox)
529553 cmd_path = Path (cmd_filepath ).absolute ()
530554
531- import subprocess
532555 import tempfile
533556
534- fd , filename = tempfile .mkstemp ()
535- # TODO: This never terminates if the passed generator never terminates.
536- text = "" .join (generator )
537- if not color :
538- text = strip_ansi (text )
539557 encoding = get_best_encoding (sys .stdout )
540- with open_stream (filename , "wb" )[0 ] as f :
541- f .write (text .encode (encoding ))
542- try :
543- subprocess .call ([str (cmd_path ), filename ])
544- except OSError :
545- # Command not found
546- pass
547- finally :
548- os .close (fd )
549- os .unlink (filename )
550-
551- return True
558+ with tempfile .NamedTemporaryFile (mode = "wb" ) as f :
559+ yield f , encoding , color
560+ f .flush ()
561+ subprocess .call ([str (cmd_path ), f .name ])
552562
553563
564+ @contextlib .contextmanager
554565def _nullpager (
555- stream : t .TextIO , generator : cabc . Iterable [ str ], color : bool | None
556- ) -> None :
566+ stream : t .TextIO , color : bool | None = None
567+ ) -> t . Iterator [ t . Tuple [ t . BinaryIO , str , bool ]] :
557568 """Simply print unformatted text. This is the ultimate fallback."""
558- for text in generator :
559- if not color :
560- text = strip_ansi (text )
561- stream .write (text )
569+ encoding = get_best_encoding (stream )
570+ yield stream , encoding , color
562571
563572
564573class Editor :
0 commit comments