Skip to content

Commit 88e28b7

Browse files
committed
init package
1 parent 9646cf2 commit 88e28b7

File tree

3 files changed

+294
-0
lines changed

3 files changed

+294
-0
lines changed

duper.py

+245
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
"""Capture C-level FD output on pipes
2+
3+
Use `duper.capture` or `duper.redirect_to_sys` as context managers.
4+
"""
5+
from __future__ import print_function
6+
7+
__version__ = '0.0.1'
8+
9+
__all__ = [
10+
'capture',
11+
'redirect_to_sys',
12+
'Duper',
13+
]
14+
15+
from contextlib import contextmanager
16+
import ctypes
17+
from fcntl import fcntl, F_GETFL, F_SETFL
18+
import io
19+
import os
20+
import select
21+
import sys
22+
import threading
23+
24+
libc = ctypes.CDLL(None)
25+
26+
try:
27+
c_stdout_p = ctypes.c_void_p.in_dll(libc, 'stdout')
28+
c_stderr_p = ctypes.c_void_p.in_dll(libc, 'stderr')
29+
except ValueError:
30+
# libc.stdout is has a funny name on OS X
31+
c_stdout_p = ctypes.c_void_p.in_dll(libc, '__stdoutp')
32+
c_stderr_p = ctypes.c_void_p.in_dll(libc, '__stderrp')
33+
34+
STDOUT = 2
35+
PIPE = 3
36+
37+
_default_encoding = getattr(sys.stdin, 'encoding', None) or 'utf8'
38+
if _default_encoding.lower() == 'ascii':
39+
# don't respect ascii
40+
_default_encoding = 'utf8'
41+
42+
class Duper(object):
43+
"""Class for Capturing Process-level FD output via dup2
44+
45+
Typically used via `duper.capture`
46+
"""
47+
flush_interval = 0.2
48+
49+
def __init__(self, stdout=None, stderr=None, encoding=_default_encoding):
50+
"""
51+
Parameters
52+
----------
53+
stdout: stream or None
54+
The stream for forwarding stdout.
55+
stderr = stream or None
56+
The stream for forwarding stderr.
57+
encoding: str or None
58+
The encoding to use, if streams should be interpreted as text.
59+
"""
60+
self._stdout = stdout
61+
if stderr == STDOUT:
62+
self._stderr = self._stdout
63+
else:
64+
self._stderr = stderr
65+
self.encoding = encoding
66+
self._save_fds = {}
67+
self._real_fds = {}
68+
self._out_pipes = {}
69+
self._handlers = {}
70+
self._handlers['stderr'] = self._handle_stderr
71+
self._handlers['stdout'] = self._handle_stdout
72+
73+
def _setup_pipe(self, name):
74+
real_fd = getattr(sys, '__%s__' % name).fileno()
75+
save_fd = os.dup(real_fd)
76+
self._save_fds[name] = save_fd
77+
78+
pipe_out, pipe_in = os.pipe()
79+
os.dup2(pipe_in, real_fd)
80+
os.close(pipe_in)
81+
self._real_fds[name] = real_fd
82+
83+
# make pipe_out non-blocking
84+
flags = fcntl(pipe_out, F_GETFL)
85+
fcntl(pipe_out, F_SETFL, flags|os.O_NONBLOCK)
86+
return pipe_out
87+
88+
def _decode(self, data):
89+
"""Decode data, if any
90+
91+
Called before pasing to stdout/stderr streams
92+
"""
93+
if self.encoding:
94+
data = data.decode(self.encoding, 'replace')
95+
return data
96+
97+
def _handle_stdout(self, data):
98+
if self._stdout:
99+
self._stdout.write(self._decode(data))
100+
101+
def _handle_stderr(self, data):
102+
if self._stderr:
103+
self._stderr.write(self._decode(data))
104+
105+
def _setup_handle(self):
106+
"""Setup handle for output, if any"""
107+
self.handle = (self._stdout, self._stderr)
108+
109+
def _finish_handle(self):
110+
"""Finish handle, if anything should be done when it's all wrapped up."""
111+
pass
112+
113+
def __enter__(self):
114+
# setup handle
115+
self._setup_handle()
116+
117+
# create pipe for stdout
118+
pipes = []
119+
names = {}
120+
if self._stdout:
121+
pipe = self._setup_pipe('stdout')
122+
pipes.append(pipe)
123+
names[pipe] = 'stdout'
124+
if self._stderr:
125+
pipe = self._setup_pipe('stderr')
126+
pipes.append(pipe)
127+
names[pipe] = 'stderr'
128+
129+
def forwarder():
130+
"""Forward bytes on a pipe to stream messages"""
131+
while True:
132+
# flush libc's buffers before calling select
133+
libc.fflush(c_stdout_p)
134+
libc.fflush(c_stderr_p)
135+
r, w, x = select.select(pipes, [], [], self.flush_interval)
136+
if not r:
137+
# nothing to read, next iteration will flush and check again
138+
continue
139+
for pipe in r:
140+
name = names[pipe]
141+
data = os.read(pipe, 1024)
142+
if not data:
143+
# pipe closed, stop polling
144+
pipes.remove(pipe)
145+
else:
146+
handler = getattr(self, '_handle_%s' % name)
147+
handler(data)
148+
if not pipes:
149+
# pipes closed, we are done
150+
break
151+
self.thread = threading.Thread(target=forwarder)
152+
self.thread.daemon = True
153+
self.thread.start()
154+
155+
return self.handle
156+
157+
def __exit__(self, exc_type, exc_value, traceback):
158+
# flush the underlying C buffers
159+
libc.fflush(c_stdout_p)
160+
libc.fflush(c_stderr_p)
161+
# close FDs, signaling output is complete
162+
for real_fd in self._real_fds.values():
163+
os.close(real_fd)
164+
self.thread.join()
165+
166+
# close finished pipes
167+
for pipe_out in self._out_pipes.values():
168+
os.close(pipe_out)
169+
170+
# restore original state
171+
for name, real_fd in self._real_fds.items():
172+
save_fd = self._save_fds[name]
173+
os.dup2(save_fd, real_fd)
174+
os.close(save_fd)
175+
# finalize handle
176+
self._finish_handle()
177+
178+
179+
@contextmanager
180+
def capture(stdout=PIPE, stderr=PIPE, encoding=_default_encoding):
181+
"""Capture C-level stdout/stderr in a context manager.
182+
183+
The return value for the context manager is (stdout, stderr).
184+
185+
Examples
186+
--------
187+
188+
>>> with capture() as (stdout, stderr):
189+
... printf("C-level stdout")
190+
... output = stdout.read()
191+
"""
192+
stdout_pipe = stderr_pipe = False
193+
# setup stdout
194+
if stdout == PIPE:
195+
stdout_r, stdout_w = os.pipe()
196+
stdout_w = os.fdopen(stdout_w, 'wb')
197+
stdout_r = os.fdopen(stdout_r, 'rb')
198+
if encoding:
199+
stdout_r = io.TextIOWrapper(stdout_r, encoding=encoding)
200+
stdout_pipe = True
201+
else:
202+
stdout_r = stdout_w = stdout
203+
# setup stderr
204+
if stderr == STDOUT:
205+
stderr_r = None
206+
stderr_w = stdout_w
207+
elif stderr == PIPE:
208+
stderr_r, stderr_w = os.pipe()
209+
stderr_w = os.fdopen(stderr_w, 'wb')
210+
stderr_r = os.fdopen(stderr_r, 'rb')
211+
if encoding:
212+
stderr_r = io.TextIOWrapper(stderr_r, encoding=encoding)
213+
stderr_pipe = True
214+
else:
215+
stderr_r = stderr_w = stderr
216+
if stdout_pipe or stderr_pipe:
217+
capture_encoding = None
218+
else:
219+
capture_encoding = encoding
220+
duper = Duper(stdout=stdout_w, stderr=stderr_w, encoding=capture_encoding)
221+
try:
222+
with duper:
223+
yield stdout_r, stderr_r
224+
finally:
225+
# close pipes
226+
if stdout_pipe:
227+
stdout_w.close()
228+
if stderr_pipe:
229+
stderr_w.close()
230+
231+
232+
def redirect_to_sys(encoding=_default_encoding):
233+
"""Redirect C-level stdout/stderr to sys.stdout/stderr
234+
235+
This is useful of sys.sdout/stderr are already being forwarded somewhere.
236+
237+
DO NOT USE THIS if sys.stdout and sys.stderr are not already being forwarded.
238+
"""
239+
return capture(sys.stdout, sys.stderr, encoding=encoding)
240+
241+
def redirect_everything_to_sys(encoding=_default_encoding):
242+
"""Redirect all C output to sys.stdout/err
243+
244+
This does *not*
245+
"""

setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[bdist_wheel]
2+
universal=1

setup.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env python
2+
import sys
3+
4+
from distutils.core import setup
5+
6+
long_description = """
7+
Context managers for capturing C-level output::
8+
9+
from duper import capture
10+
11+
with capture() as (stdout, stderr):
12+
call_c_function()
13+
out = stdout.read()
14+
err = stderr.read()
15+
"""
16+
17+
version_ns = {}
18+
with open('duper.py') as f:
19+
for line in f:
20+
if line.startswith('__version__'):
21+
exec(line, version_ns)
22+
setup_args = dict(
23+
name='duper',
24+
version=version_ns['__version__'],
25+
author="Min RK",
26+
author_email="[email protected]",
27+
description="Capture C-level output in context managers",
28+
long_description=long_description,
29+
url="https://github.com/minrk/duper",
30+
py_modules=['duper.py'],
31+
license="MIT",
32+
cmdclass={},
33+
classifiers=[
34+
"Development Status :: 3 - Alpha",
35+
"Intended Audience :: Developers",
36+
"License :: OSI Approved :: MIT License",
37+
"Programming Language :: Python :: 2.7",
38+
"Programming Language :: Python :: 3",
39+
],
40+
)
41+
42+
if 'bdist_wheel' in sys.argv:
43+
from wheel.bdist_wheel import bdist_wheel
44+
setup_args['cmdclass']['bdist_wheel'] = bdist_wheel
45+
46+
if __name__ == '__main__':
47+
setup(**setup_args)

0 commit comments

Comments
 (0)