|
| 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