Skip to content

Commit 728b31e

Browse files
authored
Merge pull request #111 from bskinn/release-1.0.1.1
Back-merge v1.0.1.1 release branch
2 parents 0b9dd54 + 2f49f59 commit 728b31e

File tree

14 files changed

+251
-210
lines changed

14 files changed

+251
-210
lines changed

.github/workflows/all_core_tests.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
name: 'PR: Run core tests'
22

3-
on: pull_request
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
48

59
jobs:
610
all_checks:

LICENSE.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2018-2019 Brian Skinn
3+
Copyright (c) 2018-2025 Brian Skinn
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
include LICENSE.txt README.rst CHANGELOG.md pyproject.toml
1+
include LICENSE.txt README.md CHANGELOG.md pyproject.toml
22
include requirements-dev.txt requirements-ci.txt requirements-flake8.txt
33
include tox.ini
44
include conftest.py

README.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
## stdio-mgr: Context manager for mocking/wrapping `stdin`/`stdout`/`stderr`
2+
3+
#### Current Development Version:
4+
5+
[![GitHub Workflow Status][workflow badge]][workflow link target]
6+
7+
#### Most Recent Stable Release
8+
9+
[![PyPI Version][pypi badge]][pypi link target]
10+
![Python Versions][python versions badge]
11+
12+
#### Info
13+
14+
[![MIT License][license badge]][license link target]
15+
[![black formatted][black badge]][black link target]
16+
[![PePY stats][pepy badge]][pepy link target]
17+
18+
----
19+
20+
### Have a CLI Python application?
21+
22+
_Want to automate testing of the actual console input & output of your
23+
user-facing components?_
24+
25+
#### `stdio-mgr` can help
26+
27+
`stdio-mgr` is a context manager for mocking/managing all three standard I/O
28+
streams: `stdout`, `stderr`, and `stdin`. While some functionality here is more
29+
or less duplicative of `redirect_stdout` and `redirect_stderr` in
30+
`contextlib` [within the standard library][stdlib redirect_stdout],
31+
it provides (i) a much more concise way to mock both `stdout` and `stderr`
32+
at the same time, and (ii) a mechanism for mocking `stdin`, which is not
33+
available in `contextlib`.
34+
35+
**First, install:**
36+
37+
```bash
38+
$ pip install stdio-mgr
39+
```
40+
41+
Then use!
42+
43+
All of the below examples assume `stdio_mgr` has already been imported via:
44+
45+
```py
46+
from stdio_mgr import stdio_mgr
47+
```
48+
49+
**Mock `stdout`:**
50+
51+
```py
52+
>>> with stdio_mgr() as (in_, out_, err_):
53+
... print('foobar')
54+
... out_cap = out_.getvalue()
55+
>>> out_cap
56+
'foobar\n'
57+
>>> in_.closed and out_.closed and err_.closed
58+
True
59+
60+
```
61+
62+
By default `print` [appends a newline][print newline] after each argument, which
63+
is why `out_cap` is `'foobar\n'` and not just `'foobar'`.
64+
65+
As currently implemented, `stdio_mgr` closes all three mocked streams upon
66+
exiting the managed context.
67+
68+
69+
**Mock `stderr`:**
70+
71+
```py
72+
>>> import warnings
73+
>>> with stdio_mgr() as (in_, out_, err_):
74+
... warnings.warn("'foo' has no 'bar'")
75+
... err_cap = err_.getvalue()
76+
>>> err_cap
77+
'... UserWarning: \'foo\' has no \'bar\'\n...'
78+
79+
```
80+
81+
82+
**Mock `stdin`:**
83+
84+
The simulated user input has to be pre-loaded to the mocked stream. **Be sure to
85+
include newlines in the input to correspond to each mocked** `Enter`
86+
**keypress!** Otherwise, `input` will hang, waiting for a newline that will
87+
never come.
88+
89+
If the entirety of the input is known in advance, it can just be provided as an
90+
argument to `stdio_mgr`. Otherwise, `.append()` mocked input to `in_` within the
91+
managed context as needed:
92+
93+
```py
94+
>>> with stdio_mgr('foobar\n') as (in_, out_, err_):
95+
... print('baz')
96+
... in_cap = input('??? ')
97+
...
98+
... _ = in_.append(in_cap[:3] + '\n')
99+
... in_cap2 = input('??? ')
100+
...
101+
... out_cap = out_.getvalue()
102+
>>> in_cap
103+
'foobar'
104+
>>> in_cap2
105+
'foo'
106+
>>> out_cap
107+
'baz\n??? foobar\n??? foo\n'
108+
109+
```
110+
111+
The `_ =` assignment suppresses `print`ing of the return value from the
112+
`in_.append()` call—otherwise, it would be interleaved in `out_cap`, since this
113+
example is shown for an interactive context. For non-interactive execution, as
114+
with `unittest`, `pytest`, etc., these 'muting' assignments should not be
115+
necessary.
116+
117+
**Both** the `'??? '` prompts for `input` **and** the mocked input strings are
118+
echoed to `out_`, mimicking what a CLI user would see.
119+
120+
A subtlety: While the trailing newline on, e.g., `'foobar\n'` is stripped by
121+
`input`, it is *retained* in `out_`. This is because `in_` tees the content read
122+
from it to `out_` *before* that content is passed to `input`.
123+
124+
125+
#### Want to modify internal `print` calls within a function or method?
126+
127+
In addition to mocking, `stdio_mgr` can also be used to wrap functions that
128+
directly output to `stdout`/`stderr`. A `stdout` example:
129+
130+
```py
131+
>>> def emboxen(func):
132+
... def func_wrapper(s):
133+
... from stdio_mgr import stdio_mgr
134+
...
135+
... with stdio_mgr() as (in_, out_, err_):
136+
... func(s)
137+
... content = out_.getvalue()
138+
...
139+
... max_len = max(map(len, content.splitlines()))
140+
... fmt_str = '| {{: <{0}}} |\n'.format(max_len)
141+
...
142+
... newcontent = '=' * (max_len + 4) + '\n'
143+
... for line in content.splitlines():
144+
... newcontent += fmt_str.format(line)
145+
... newcontent += '=' * (max_len + 4)
146+
...
147+
... print(newcontent)
148+
...
149+
... return func_wrapper
150+
151+
>>> @emboxen
152+
... def testfunc(s):
153+
... print(s)
154+
155+
>>> testfunc("""\
156+
... Foo bar baz quux.
157+
... Lorem ipsum dolor sit amet.""")
158+
===============================
159+
| Foo bar baz quux. |
160+
| Lorem ipsum dolor sit amet. |
161+
===============================
162+
163+
```
164+
165+
----
166+
167+
Available on [PyPI][pypi link target] (`pip install stdio-mgr`).
168+
169+
Source on [GitHub][gh repo]. Bug reports and feature requests are welcomed at
170+
the [Issues][gh issues] page there.
171+
172+
Copyright \(c) 2018-2025 Brian Skinn
173+
174+
The `stdio-mgr` documentation (currently docstrings and README) is licensed
175+
under a [Creative Commons Attribution 4.0 International License][cc-by] (CC-BY).
176+
The `stdio-mgr` codebase is released under the [MIT License]. See
177+
[`LICENSE.txt`] for full license terms.
178+
179+
180+
[`LICENSE.txt`]: https://github.com/bskinn/flake8-absolute-import/blob/main/LICENSE.txt
181+
182+
[black badge]: https://img.shields.io/badge/code%20style-black-000000.svg
183+
[black link target]: https://github.com/psf/black
184+
185+
[cc-by]: http://creativecommons.org/licenses/by/4.0/
186+
187+
[gh issues]: https://github.com/bskinn/stdio-mgr/issues
188+
[gh repo]: https://github.com/bskinn/stdio-mgr
189+
190+
[license badge]: https://img.shields.io/github/license/mashape/apistatus.svg
191+
[license link target]: https://github.com/bskinn/stdio-mgr/blob/stable/LICENSE.txt
192+
193+
[MIT License]: https://opensource.org/licenses/MIT
194+
195+
[pepy badge]: https://pepy.tech/badge/stdio-mgr/month
196+
[pepy link target]: https://pepy.tech/projects/stdio-mgr?timeRange=threeMonths&category=version&includeCIDownloads=true&granularity=daily&viewType=line&versions=1.0.1%2C1.0.1.1
197+
198+
[print newline]: https://docs.python.org/3/library/functions.html#print
199+
200+
[pypi badge]: https://img.shields.io/pypi/v/stdio-mgr.svg?logo=pypi
201+
[pypi link target]: https://pypi.org/project/stdio-mgr
202+
203+
[python versions badge]: https://img.shields.io/pypi/pyversions/stdio-mgr.svg?logo=python
204+
205+
[stdlib redirect_stdout]: https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout
206+
207+
[workflow badge]: https://img.shields.io/github/actions/workflow/status/bskinn/stdio-mgr/all_core_tests.yml?branch=main&logo=github
208+
[workflow link target]: https://github.com/bskinn/stdio-mgr/actions

0 commit comments

Comments
 (0)