Skip to content

Commit b7045f3

Browse files
committed
Refactor the stream Director to support code accessing its .buffer
This enables having the stream Directors installed while running tox including isatty terminal features. Newer version of tox directly access objects on self.stdout.buffer. Note: This doesn't make use of the buffer. Director.write always calls flush for every call.
1 parent 6bc6754 commit b7045f3

File tree

1 file changed

+87
-16
lines changed

1 file changed

+87
-16
lines changed

preditor/stream/director.py

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,61 @@
66
from . import STDERR, STDOUT
77

88

9-
class Director(io.TextIOBase):
9+
class _DirectorBuffer(io.RawIOBase):
10+
"""Binary buffer that forwards text writes to the manager.
11+
12+
This makes the stream more compatible including if enabled when running tox.
13+
14+
Args:
15+
manager (Manager): The manager that writes are stored in.
16+
state: The state passed to the manager. This is often ``preditor.stream.STDOUT``
17+
or ``preditor.stream.STDERR``.
18+
old_stream: A second stream that will be written to every time this stream
19+
is written to. This allows this object to replace sys.stdout and still
20+
send that output to the original stdout, which is useful for not breaking
21+
DCC's script editors. Pass False to disable this feature. If you pass None
22+
and state is set to ``preditor.stream.STDOUT`` or ``preditor.stream.STDERR``
23+
this will automatically be set to the current sys.stdout or sys.stderr.
24+
name (str, optional): Stored on self.name.
25+
"""
26+
27+
def __init__(self, manager, state, old_stream=None, name='nul'):
28+
super().__init__()
29+
self.manager = manager
30+
self.state = state
31+
self.old_stream = old_stream
32+
self.name = name
33+
34+
def flush(self):
35+
if self.old_stream:
36+
self.old_stream.flush()
37+
super().flush()
38+
39+
def writable(self):
40+
return True
41+
42+
def write(self, b):
43+
if isinstance(b, memoryview):
44+
b = b.tobytes()
45+
46+
# Decode incoming bytes (TextIOWrapper encodes before sending here)
47+
msg = b.decode("utf-8", errors="replace")
48+
self.manager.write(msg, self.state)
49+
50+
if self.old_stream:
51+
self.old_stream.write(msg)
52+
53+
return len(b)
54+
55+
56+
class Director(io.TextIOWrapper):
1057
"""A file like object that stores the text written to it in a manager.
1158
This manager can be shared between multiple Directors to build a single
1259
continuous history of all writes.
1360
61+
While this uses a buffer under the hood, buffering is disabled and any calls
62+
to write will automatically flush the buffer.
63+
1464
Args:
1565
manager (Manager): The manager that writes are stored in.
1666
state: The state passed to the manager. This is often ``preditor.stream.STDOUT``
@@ -24,30 +74,40 @@ class Director(io.TextIOBase):
2474
"""
2575

2676
def __init__(self, manager, state, old_stream=None, *args, **kwargs):
27-
super(Director, self).__init__(*args, **kwargs)
28-
self.manager = manager
29-
self.state = state
30-
3177
# Keep track of whether we wrapped a std stream
3278
# that way we don't .close() any streams that we don't control
3379
self.std_stream_wrapped = False
3480

81+
name = 'nul'
3582
if old_stream is False:
3683
old_stream = None
3784
elif old_stream is None:
3885
if state == STDOUT:
3986
# On Windows if we're in pythonw.exe, then sys.stdout is named "nul"
4087
# And it uses cp1252 encoding (which breaks with unicode)
4188
# So if we find this nul TextIOWrapper, it's safe to just skip it
42-
if getattr(sys.stdout, 'name', '') != 'nul':
89+
name = getattr(sys.stdout, 'name', '')
90+
if name != 'nul':
4391
self.std_stream_wrapped = True
4492
old_stream = sys.stdout
4593
elif state == STDERR:
46-
if getattr(sys.stderr, 'name', '') != 'nul':
94+
name = getattr(sys.stderr, 'name', '')
95+
if name != 'nul':
4796
self.std_stream_wrapped = True
4897
old_stream = sys.stderr
4998

5099
self.old_stream = old_stream
100+
self.manager = manager
101+
self.state = state
102+
103+
# Build the buffer. This provides the expected interface for tox, etc.
104+
raw = _DirectorBuffer(manager, state, old_stream, name)
105+
buffer = io.BufferedWriter(raw)
106+
107+
super().__init__(buffer, encoding="utf-8", write_through=True, *args, **kwargs)
108+
109+
def __repr__(self):
110+
return f"<Director state={self.state} old_stream={self.old_stream!r}>"
51111

52112
def close(self):
53113
if (
@@ -58,16 +118,27 @@ def close(self):
58118
):
59119
self.old_stream.close()
60120

61-
super(Director, self).close()
121+
super().close()
62122

63-
def flush(self):
64-
if self.old_stream:
65-
self.old_stream.flush()
123+
def write(self, msg):
124+
super().write(msg)
125+
# Force a write of any buffered data
126+
self.flush()
66127

67-
super(Director, self).flush()
128+
# These methods enable terminal features like color coding etc.
129+
def isatty(self):
130+
if self.old_stream is not None:
131+
return self.old_stream.isatty()
132+
return False
68133

69-
def write(self, msg):
70-
self.manager.write(msg, self.state)
134+
@property
135+
def encoding(self):
136+
if self.old_stream is not None:
137+
return self.old_stream.encoding
138+
return super().encoding
71139

72-
if self.old_stream:
73-
self.old_stream.write(msg)
140+
@property
141+
def errors(self):
142+
if self.old_stream is not None:
143+
return self.old_stream.errors
144+
return super().errors

0 commit comments

Comments
 (0)