17
17
Multiple applications can run in the body of the context manager, one after the
18
18
other.
19
19
"""
20
+ import asyncio
21
+ import queue
20
22
import sys
21
23
import threading
22
- from asyncio import get_event_loop
24
+ import time
23
25
from contextlib import contextmanager
24
- from typing import Generator , List , Optional , TextIO , cast
26
+ from typing import Generator , List , Optional , TextIO , Union , cast
25
27
26
- from .application import run_in_terminal
28
+ from .application import get_app_session , run_in_terminal
29
+ from .output import Output
27
30
28
31
__all__ = [
29
32
"patch_stdout" ,
@@ -49,71 +52,175 @@ def patch_stdout(raw: bool = False) -> Generator[None, None, None]:
49
52
:param raw: (`bool`) When True, vt100 terminal escape sequences are not
50
53
removed/escaped.
51
54
"""
52
- proxy = cast (TextIO , StdoutProxy (raw = raw ))
55
+ with StdoutProxy (raw = raw ) as proxy :
56
+ original_stdout = sys .stdout
57
+ original_stderr = sys .stderr
53
58
54
- original_stdout = sys .stdout
55
- original_stderr = sys .stderr
59
+ # Enter.
60
+ sys .stdout = cast (TextIO , proxy )
61
+ sys .stderr = cast (TextIO , proxy )
56
62
57
- # Enter.
58
- sys .stdout = proxy
59
- sys .stderr = proxy
63
+ try :
64
+ yield
65
+ finally :
66
+ sys .stdout = original_stdout
67
+ sys .stderr = original_stderr
60
68
61
- try :
62
- yield
63
- finally :
64
- # Exit.
65
- proxy .flush ()
66
69
67
- sys . stdout = original_stdout
68
- sys . stderr = original_stderr
70
+ class _Done :
71
+ " Sentinel value for stopping the stdout proxy. "
69
72
70
73
71
74
class StdoutProxy :
72
75
"""
73
- Proxy object for stdout which captures everything and prints output above
74
- the current application.
76
+ File-like object, which prints everything written to it, output above the
77
+ current application/prompt. This class is compatible with other file
78
+ objects and can be used as a drop-in replacement for `sys.stdout` or can
79
+ for instance be passed to `logging.StreamHandler`.
80
+
81
+ The current application, above which we print, is determined by looking
82
+ what application currently runs in the `AppSession` that is active during
83
+ the creation of this instance.
84
+
85
+ This class can be used as a context manager.
86
+
87
+ In order to avoid having to repaint the prompt continuously for every
88
+ little write, a short delay of `sleep_between_writes` seconds will be added
89
+ between writes in order to bundle many smaller writes in a short timespan.
75
90
"""
76
91
77
92
def __init__ (
78
- self , raw : bool = False , original_stdout : Optional [TextIO ] = None
93
+ self ,
94
+ sleep_between_writes : float = 0.2 ,
95
+ raw : bool = False ,
79
96
) -> None :
80
97
81
- original_stdout = original_stdout or sys .__stdout__
82
-
83
- self .original_stdout = original_stdout
98
+ self .sleep_between_writes = sleep_between_writes
99
+ self .raw = raw
84
100
85
101
self ._lock = threading .RLock ()
86
- self ._raw = raw
87
102
self ._buffer : List [str ] = []
88
103
89
- # errors/encoding attribute for compatibility with sys.__stdout__.
90
- self .errors = original_stdout .errors
91
- self .encoding = original_stdout .encoding
104
+ # Keep track of the curret app session.
105
+ self .app_session = get_app_session ()
106
+
107
+ # See what output is active *right now*. We should do it at this point,
108
+ # before this `StdoutProxy` instance is possibly assigned to `sys.stdout`.
109
+ # Otherwise, if `patch_stdout` is used, and no `Output` instance has
110
+ # been created, then the default output creation code will see this
111
+ # proxy object as `sys.stdout`, and get in a recursive loop trying to
112
+ # access `StdoutProxy.isatty()` which will again retrieve the output.
113
+ self ._output : Output = self .app_session .output
114
+
115
+ # Flush thread
116
+ self ._flush_queue : queue .Queue [Union [str , _Done ]] = queue .Queue ()
117
+ self ._flush_thread = self ._start_write_thread ()
118
+ self .closed = False
119
+
120
+ def __enter__ (self ) -> "StdoutProxy" :
121
+ return self
92
122
93
- self .loop = get_event_loop ()
123
+ def __exit__ (self , * args ) -> None :
124
+ self .close ()
94
125
95
- def _write_and_flush (self , text : str ) -> None :
126
+ def close (self ) -> None :
127
+ """
128
+ Stop `StdoutProxy` proxy.
129
+
130
+ This will terminate the write thread, make sure everything is flushed
131
+ and wait for the write thread to finish.
132
+ """
133
+ if not self .closed :
134
+ self ._flush_queue .put (_Done ())
135
+ self ._flush_thread .join ()
136
+ self .closed = True
137
+
138
+ def _start_write_thread (self ) -> threading .Thread :
139
+ thread = threading .Thread (
140
+ target = self ._write_thread ,
141
+ name = "patch-stdout-flush-thread" ,
142
+ daemon = True ,
143
+ )
144
+ thread .start ()
145
+ return thread
146
+
147
+ def _write_thread (self ) -> None :
148
+ done = False
149
+
150
+ while not done :
151
+ item = self ._flush_queue .get ()
152
+
153
+ if isinstance (item , _Done ):
154
+ break
155
+
156
+ # Don't bother calling when we got an empty string.
157
+ if not item :
158
+ continue
159
+
160
+ text = []
161
+ text .append (item )
162
+
163
+ # Read the rest of the queue if more data was queued up.
164
+ while True :
165
+ try :
166
+ item = self ._flush_queue .get_nowait ()
167
+ except queue .Empty :
168
+ break
169
+ else :
170
+ if isinstance (item , _Done ):
171
+ done = True
172
+ else :
173
+ text .append (item )
174
+
175
+ app_loop = self ._get_app_loop ()
176
+ self ._write_and_flush (app_loop , "" .join (text ))
177
+
178
+ # If an application was running that requires repainting, then wait
179
+ # for a very short time, in order to bundle actual writes and avoid
180
+ # having to repaint to often.
181
+ if app_loop is not None :
182
+ time .sleep (self .sleep_between_writes )
183
+
184
+ def _get_app_loop (self ) -> Optional [asyncio .AbstractEventLoop ]:
185
+ """
186
+ Return the event loop for the application currently running in our
187
+ `AppSession`.
188
+ """
189
+ app = self .app_session .app
190
+
191
+ if app is None :
192
+ return None
193
+
194
+ return app .loop
195
+
196
+ def _write_and_flush (
197
+ self , loop : Optional [asyncio .AbstractEventLoop ], text : str
198
+ ) -> None :
96
199
"""
97
200
Write the given text to stdout and flush.
98
201
If an application is running, use `run_in_terminal`.
99
202
"""
100
- if not text :
101
- # Don't bother calling `run_in_terminal` when there is nothing to
102
- # display.
103
- return
104
203
105
204
def write_and_flush () -> None :
106
- self .original_stdout .write (text )
107
- self .original_stdout .flush ()
205
+ if self .raw :
206
+ self ._output .write_raw (text )
207
+ else :
208
+ self ._output .write (text )
209
+
210
+ self ._output .flush ()
108
211
109
212
def write_and_flush_in_loop () -> None :
110
213
# If an application is running, use `run_in_terminal`, otherwise
111
214
# call it directly.
112
- run_in_terminal . run_in_terminal (write_and_flush , in_executor = False )
215
+ run_in_terminal (write_and_flush , in_executor = False )
113
216
114
- # Make sure `write_and_flush` is executed *in* the event loop, not in
115
- # another thread.
116
- self .loop .call_soon_threadsafe (write_and_flush_in_loop )
217
+ if loop is None :
218
+ # No loop, write immediately.
219
+ write_and_flush ()
220
+ else :
221
+ # Make sure `write_and_flush` is executed *in* the event loop, not
222
+ # in another thread.
223
+ loop .call_soon_threadsafe (write_and_flush_in_loop )
117
224
118
225
def _write (self , data : str ) -> None :
119
226
"""
@@ -133,15 +240,15 @@ def _write(self, data: str) -> None:
133
240
self ._buffer = [after ]
134
241
135
242
text = "" .join (to_write )
136
- self ._write_and_flush (text )
243
+ self ._flush_queue . put (text )
137
244
else :
138
245
# Otherwise, cache in buffer.
139
246
self ._buffer .append (data )
140
247
141
248
def _flush (self ) -> None :
142
249
text = "" .join (self ._buffer )
143
250
self ._buffer = []
144
- self ._write_and_flush (text )
251
+ self ._flush_queue . put (text )
145
252
146
253
def write (self , data : str ) -> int :
147
254
with self ._lock :
@@ -156,12 +263,26 @@ def flush(self) -> None:
156
263
with self ._lock :
157
264
self ._flush ()
158
265
266
+ @property
267
+ def original_stdout (self ) -> TextIO :
268
+ return self ._output .stdout or sys .__stdout__
269
+
270
+ # Attributes for compatibility with sys.__stdout__:
271
+
159
272
def fileno (self ) -> int :
160
- """
161
- Return file descriptor.
162
- """
163
- # This is important for code that expects sys.stdout.fileno() to work.
164
- return self .original_stdout .fileno ()
273
+ return self ._output .fileno ()
165
274
166
275
def isatty (self ) -> bool :
167
- return self .original_stdout .isatty ()
276
+ stdout = self ._output .stdout
277
+ if stdout is None :
278
+ return False
279
+
280
+ return stdout .isatty ()
281
+
282
+ @property
283
+ def encoding (self ) -> str :
284
+ return self ._output .encoding ()
285
+
286
+ @property
287
+ def errors (self ) -> str :
288
+ return "strict"
0 commit comments