pax_global_header00006660000000000000000000000064151125076010014510gustar00rootroot0000000000000052 comment=8c8f7091e6308885afec08adaba14bd98644a9ed bskinn-stdio-mgr-8c8f709/000077500000000000000000000000001511250760100152515ustar00rootroot00000000000000bskinn-stdio-mgr-8c8f709/.coveragerc000066400000000000000000000001721511250760100173720ustar00rootroot00000000000000[run] include = src/* tests/* omit = env*/* [report] exclude_lines = pragma: no cover ^\s*pass\s*$ bskinn-stdio-mgr-8c8f709/.github/000077500000000000000000000000001511250760100166115ustar00rootroot00000000000000bskinn-stdio-mgr-8c8f709/.github/workflows/000077500000000000000000000000001511250760100206465ustar00rootroot00000000000000bskinn-stdio-mgr-8c8f709/.github/workflows/all_core_tests.yml000066400000000000000000000033721511250760100244000ustar00rootroot00000000000000name: 'PR: Run core tests' on: pull_request: push: branches: - main jobs: all_checks: name: plus lints, etc. on Python 3.12 runs-on: ubuntu-latest if: ${{ !contains(toJson(github.event), '[skip ci]') }} steps: - name: Check out repo uses: actions/checkout@v6 - name: Install Python uses: actions/setup-python@v6 with: python-version: '3.12' cache: 'pip' cache-dependency-path: | requirements-ci.txt requirements-flake8.txt - name: Update pip & setuptools run: python -m pip install -U pip setuptools - name: Install & report CI dependencies run: | python -m pip install -U --force-reinstall -r requirements-ci.txt -r requirements-flake8.txt python --version pip list - name: Run tests run: | pytest --cov tox -e sdist_install - name: Lint code run: tox -e flake8 just_tests: name: for Python ${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: python: ['3.10', '3.11', '3.13', '3.14'] if: ${{ !contains(toJson(github.event), '[skip ci]') }} steps: - name: Check out repo uses: actions/checkout@v6 - name: Install Python uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} cache: 'pip' cache-dependency-path: requirements-ci.txt - name: Update pip & setuptools run: python -m pip install -U pip setuptools - name: Install & report CI dependencies run: | python -m pip install -U --force-reinstall -r requirements-ci.txt python --version pip list - name: Run tests run: | pytest --cov tox -e sdist_install bskinn-stdio-mgr-8c8f709/.github/workflows/release_platform_tests.yml000066400000000000000000000023331511250760100261400ustar00rootroot00000000000000name: 'RELEASE: Run cross-platform tests' on: pull_request: types: - opened - reopened - synchronize - ready_for_review branches: - stable jobs: just_tests: name: ${{ matrix.os }} python ${{ matrix.py }} runs-on: ${{ matrix.os }} concurrency: group: ${{ github.workflow }}-${{ matrix.os }}-${{ matrix.py }}-${{ github.ref }} cancel-in-progress: true if: ${{ !github.event.pull_request.draft && !contains(toJson(github.event), '[skip ci]') }} strategy: matrix: os: ['windows-latest', 'macos-latest'] py: ['3.10', '3.11', '3.12', '3.13'] steps: - name: Check out repo uses: actions/checkout@v6 - name: Install Python uses: actions/setup-python@v6 with: python-version: ${{ matrix.py }} cache: 'pip' cache-dependency-path: requirements-ci.txt - name: Update pip & setuptools run: python -m pip install -U pip setuptools - name: Install & report CI dependencies run: | python -m pip install -U --force-reinstall -r requirements-ci.txt python --version pip list - name: Run tests run: | pytest --cov tox -e sdist_install bskinn-stdio-mgr-8c8f709/.github/workflows/release_sdist_test.yml000066400000000000000000000032511511250760100252570ustar00rootroot00000000000000name: 'RELEASE: Check sdist' on: pull_request: types: - opened - reopened - synchronize - ready_for_review branches: - stable jobs: sdist_build_and_check: name: builds & is testable runs-on: 'ubuntu-latest' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true if: ${{ !github.event.pull_request.draft && !contains(toJson(github.event), '[skip ci]')}} steps: - name: Check out repo uses: actions/checkout@v6 - name: Install Python uses: actions/setup-python@v6 with: python-version: '3.12' cache: 'pip' cache-dependency-path: | requirements-dev.txt requirements-flake8.txt - name: Build sdist run: | pip install tox tox -e build ls -lah dist - name: Create sandbox run: mkdir sandbox - name: Unpack sdist in sandbox run: | cp dist/*.gz sandbox/ cd sandbox tar xvf *.gz - name: Create venv run: | cd sandbox python -m venv env # Only the dir of the unpacked sdist will have a digit in its name - name: Store sdist unpack path run: echo "UNPACK_PATH=$( find sandbox -maxdepth 1 -type d -regex 'sandbox/.+[0-9].+' )" >> $GITHUB_ENV - name: Report sdist unpack path run: echo $UNPACK_PATH - name: Install dev req'ts to venv run: | source sandbox/env/bin/activate cd "$UNPACK_PATH" python -m pip install -r requirements-dev.txt - name: Run test suite in sandbox run: | source sandbox/env/bin/activate cd "$UNPACK_PATH" pytest bskinn-stdio-mgr-8c8f709/.github/workflows/release_testdir_coverage.yml000066400000000000000000000015421511250760100264240ustar00rootroot00000000000000name: 'RELEASE: Ensure all tests ran' on: pull_request: types: - opened - reopened - synchronize - ready_for_review branches: - stable jobs: testdir_coverage: name: via coverage check runs-on: 'ubuntu-latest' if: ${{ !github.event.pull_request.draft && !contains(toJson(github.event), '[skip ci]')}} steps: - name: Check out repo uses: actions/checkout@v6 - name: Install Python uses: actions/setup-python@v6 with: python-version: '3.12' cache: 'pip' cache-dependency-path: | requirements-ci.txt - name: Install CI requirements run: pip install -r requirements-ci.txt - name: Run pytest with coverage run: pytest --cov - name: Check 100% test execution run: coverage report --include="tests/*" --fail-under=100 bskinn-stdio-mgr-8c8f709/.gitignore000066400000000000000000000023051511250760100172410ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env*/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .xcovrc # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # Temp/backup editor files *.bak bskinn-stdio-mgr-8c8f709/CHANGELOG.md000066400000000000000000000033501511250760100170630ustar00rootroot00000000000000## CHANGELOG: stdio-mgr stdin/stdout/stderr mock/wrap context manager All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ### [Unreleased] #### Internal - Convert project build config (mostly) from `setup.py` to `pyproject.toml` ([#108]). - The dynamic README stays in `setup.py`. - Convert CI to GitHub Actions and diversify ([#108]). - Ubuntu tests across Pythons on every PR. - Cross-platform tests across Pythons on PRs to `stable`. - Ensuring testability of sdist in PRs to `stable`. - Augment `MANIFEST.in` until tests run successfully on unpacked sdist. - Checking all tests ran in PRs to `stable`. - `[skip ci]` implemented in all. - Refactor `__version__` to new `version.py` ([#108]). - Set up `black`, `flake`, `isort` with `tox` envs and run/fix ([#108]). - Update Python & deps versions in `tox` env matrix ([#108]). ### [1.0.1] - 2019-02-11 #### Changed * `TeeStdin` is now a `slots=False` attrs class, to avoid errors arising from some manner of change in the vicinity of attrs v18.1/v18.2. ### [1.0.0] - 2018-04-01 #### Features * `stdio_mgr` context manager with capability for mocking/wrapping all three of `stdin`/`stdout`/`stderr` * `stdin` mocking/wrapping is implemented with the custom `TeeStdin`, a subclass of `StringIO`, which tees all content read from itself into the mocked/wrapped `stdout` * `TeeStdin` is extended from `StringIO` by an `.append()` method, which adds content to the end of the stream without changing the current seek position. [#108]: https://github.com/bskinn/stdio-mgr/pull/108 bskinn-stdio-mgr-8c8f709/LICENSE.txt000066400000000000000000000020611511250760100170730ustar00rootroot00000000000000MIT License Copyright (c) 2018-2025 Brian Skinn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bskinn-stdio-mgr-8c8f709/MANIFEST.in000066400000000000000000000002471511250760100170120ustar00rootroot00000000000000include LICENSE.txt README.md CHANGELOG.md pyproject.toml include requirements-dev.txt requirements-ci.txt requirements-flake8.txt include tox.ini include conftest.py bskinn-stdio-mgr-8c8f709/README.md000066400000000000000000000143651511250760100165410ustar00rootroot00000000000000## stdio-mgr: Context manager for mocking/wrapping `stdin`/`stdout`/`stderr` #### Current Development Version: [![GitHub Workflow Status][workflow badge]][workflow link target] #### Most Recent Stable Release [![PyPI Version][pypi badge]][pypi link target] ![Python Versions][python versions badge] #### Info [![MIT License][license badge]][license link target] [![black formatted][black badge]][black link target] [![PePY stats][pepy badge]][pepy link target] ---- ### Have a CLI Python application? _Want to automate testing of the actual console input & output of your user-facing components?_ #### `stdio-mgr` can help `stdio-mgr` is a context manager for mocking/managing all three standard I/O streams: `stdout`, `stderr`, and `stdin`. While some functionality here is more or less duplicative of `redirect_stdout` and `redirect_stderr` in `contextlib` [within the standard library][stdlib redirect_stdout], it provides (i) a much more concise way to mock both `stdout` and `stderr` at the same time, and (ii) a mechanism for mocking `stdin`, which is not available in `contextlib`. **First, install:** ```bash $ pip install stdio-mgr ``` Then use! All of the below examples assume `stdio_mgr` has already been imported via: ```py from stdio_mgr import stdio_mgr ``` **Mock `stdout`:** ```py >>> with stdio_mgr() as (in_, out_, err_): ... print('foobar') ... out_cap = out_.getvalue() >>> out_cap 'foobar\n' >>> in_.closed and out_.closed and err_.closed True ``` By default `print` [appends a newline][print newline] after each argument, which is why `out_cap` is `'foobar\n'` and not just `'foobar'`. As currently implemented, `stdio_mgr` closes all three mocked streams upon exiting the managed context. **Mock `stderr`:** ```py >>> import warnings >>> with stdio_mgr() as (in_, out_, err_): ... warnings.warn("'foo' has no 'bar'") ... err_cap = err_.getvalue() >>> err_cap '... UserWarning: \'foo\' has no \'bar\'\n...' ``` **Mock `stdin`:** The simulated user input has to be pre-loaded to the mocked stream. **Be sure to include newlines in the input to correspond to each mocked** `Enter` **keypress!** Otherwise, `input` will hang, waiting for a newline that will never come. If the entirety of the input is known in advance, it can just be provided as an argument to `stdio_mgr`. Otherwise, `.append()` mocked input to `in_` within the managed context as needed: ```py >>> with stdio_mgr('foobar\n') as (in_, out_, err_): ... print('baz') ... in_cap = input('??? ') ... ... _ = in_.append(in_cap[:3] + '\n') ... in_cap2 = input('??? ') ... ... out_cap = out_.getvalue() >>> in_cap 'foobar' >>> in_cap2 'foo' >>> out_cap 'baz\n??? foobar\n??? foo\n' ``` The `_ =` assignment suppresses `print`ing of the return value from the `in_.append()` call—otherwise, it would be interleaved in `out_cap`, since this example is shown for an interactive context. For non-interactive execution, as with `unittest`, `pytest`, etc., these 'muting' assignments should not be necessary. **Both** the `'??? '` prompts for `input` **and** the mocked input strings are echoed to `out_`, mimicking what a CLI user would see. A subtlety: While the trailing newline on, e.g., `'foobar\n'` is stripped by `input`, it is *retained* in `out_`. This is because `in_` tees the content read from it to `out_` *before* that content is passed to `input`. #### Want to modify internal `print` calls within a function or method? In addition to mocking, `stdio_mgr` can also be used to wrap functions that directly output to `stdout`/`stderr`. A `stdout` example: ```py >>> def emboxen(func): ... def func_wrapper(s): ... from stdio_mgr import stdio_mgr ... ... with stdio_mgr() as (in_, out_, err_): ... func(s) ... content = out_.getvalue() ... ... max_len = max(map(len, content.splitlines())) ... fmt_str = '| {{: <{0}}} |\n'.format(max_len) ... ... newcontent = '=' * (max_len + 4) + '\n' ... for line in content.splitlines(): ... newcontent += fmt_str.format(line) ... newcontent += '=' * (max_len + 4) ... ... print(newcontent) ... ... return func_wrapper >>> @emboxen ... def testfunc(s): ... print(s) >>> testfunc("""\ ... Foo bar baz quux. ... Lorem ipsum dolor sit amet.""") =============================== | Foo bar baz quux. | | Lorem ipsum dolor sit amet. | =============================== ``` ---- Available on [PyPI][pypi link target] (`pip install stdio-mgr`). Source on [GitHub][gh repo]. Bug reports and feature requests are welcomed at the [Issues][gh issues] page there. Copyright \(c) 2018-2025 Brian Skinn The `stdio-mgr` documentation (currently docstrings and README) is licensed under a [Creative Commons Attribution 4.0 International License][cc-by] (CC-BY). The `stdio-mgr` codebase is released under the [MIT License]. See [`LICENSE.txt`] for full license terms. [`LICENSE.txt`]: https://github.com/bskinn/flake8-absolute-import/blob/main/LICENSE.txt [black badge]: https://img.shields.io/badge/code%20style-black-000000.svg [black link target]: https://github.com/psf/black [cc-by]: http://creativecommons.org/licenses/by/4.0/ [gh issues]: https://github.com/bskinn/stdio-mgr/issues [gh repo]: https://github.com/bskinn/stdio-mgr [license badge]: https://img.shields.io/github/license/mashape/apistatus.svg [license link target]: https://github.com/bskinn/stdio-mgr/blob/stable/LICENSE.txt [MIT License]: https://opensource.org/licenses/MIT [pepy badge]: https://pepy.tech/badge/stdio-mgr/month [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 [print newline]: https://docs.python.org/3/library/functions.html#print [pypi badge]: https://img.shields.io/pypi/v/stdio-mgr.svg?logo=pypi [pypi link target]: https://pypi.org/project/stdio-mgr [python versions badge]: https://img.shields.io/pypi/pyversions/stdio-mgr.svg?logo=python [stdlib redirect_stdout]: https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout [workflow badge]: https://img.shields.io/github/actions/workflow/status/bskinn/stdio-mgr/all_core_tests.yml?branch=main&logo=github [workflow link target]: https://github.com/bskinn/stdio-mgr/actions bskinn-stdio-mgr-8c8f709/conftest.py000066400000000000000000000013051511250760100174470ustar00rootroot00000000000000r"""*pytest configuration for the* ``stdio_mgr`` *test suite*. ``stdio_mgr`` provides a context manager for convenient mocking and/or wrapping of ``stdin``/``stdout``/``stderr`` interactions. **Author** Brian Skinn (bskinn@alum.mit.edu) **File Created** 6 Feb 2019 **Copyright** \(c) Brian Skinn 2018-2025 **Source Repository** http://www.github.com/bskinn/stdio-mgr **Documentation** See README.md at the GitHub repository **License** The MIT License; see |license_txt|_ for full license terms **Members** """ import pytest from stdio_mgr import stdio_mgr @pytest.fixture(autouse=True) def add_stdio_mgr(doctest_namespace): doctest_namespace["stdio_mgr"] = stdio_mgr bskinn-stdio-mgr-8c8f709/pyproject.toml000066400000000000000000000034121511250760100201650ustar00rootroot00000000000000[build-system] requires = [ "wheel", "setuptools>77", ] build-backend = "setuptools.build_meta" [project] name = "stdio-mgr" description = "Context manager for mocking/wrapping stdin/stdout/stderr" authors = [ { name = "Brian Skinn", email = "brian.skinn@gmail.com" }, ] license = "MIT" license-files = [ "LICENSE.txt", ] classifiers = [ "Natural Language :: English", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Testing :: Mocking", "Topic :: Software Development :: User Interfaces", "Development Status :: 5 - Production/Stable", ] keywords = [ "stdin", "stdout", "stderr", "mock", ] requires-python = ">=3.10" dependencies = [ "attrs>=17.1", ] dynamic = [ "version", "readme", ] [project.urls] Homepage = "https://github.com/bskinn/stdio-mgr" Changelog = "https://github.com/bskinn/stdio-mgr/blob/main/CHANGELOG.md" Donate = "https://github.com/sponsors/bskinn" [tool.setuptools] platforms = [ "any", ] include-package-data = false [tool.setuptools.dynamic] version = {attr = "stdio_mgr.version.__version__"} [tool.setuptools.package-dir] "" = "src" [tool.setuptools.packages.find] where = [ "src", ] namespaces = false [tool.black] line-length = 88 include = ''' ( ^/tests/ | ^/src/stdio_mgr/ | ^/setup[.]py | ^/conftest[.]py ) ''' exclude = ''' ( __pycache__ ) '''bskinn-stdio-mgr-8c8f709/requirements-ci.txt000066400000000000000000000000451511250760100211250ustar00rootroot00000000000000attrs>=17 pytest pytest-cov tox -e . bskinn-stdio-mgr-8c8f709/requirements-dev.txt000066400000000000000000000001011511250760100213010ustar00rootroot00000000000000attrs>=17 ipython pytest pytest-cov restview tox twine wget -e . bskinn-stdio-mgr-8c8f709/requirements-flake8.txt000066400000000000000000000003441511250760100217060ustar00rootroot00000000000000flake8>=3.7 flake8-absolute-import flake8-bandit flake8-black flake8-bugbear flake8-builtins flake8-comprehensions flake8-docstrings flake8-eradicate flake8-import-order flake8-isort flake8-pie flake8-rst-docstrings pep8-naming bskinn-stdio-mgr-8c8f709/setup.py000066400000000000000000000021141511250760100167610ustar00rootroot00000000000000import re from pathlib import Path from typing import Any, cast from setuptools import setup NAME = "stdio-mgr" exec_ns: dict[str, Any] = {} exec(Path("src", "stdio_mgr", "version.py").read_text(encoding="utf-8"), exec_ns) __version__ = cast(str, exec_ns["__version__"]) version_override: str | None = None def readme(): content = Path("README.md").read_text(encoding="utf-8") new_ver = version_override if version_override else __version__ # Helper function def content_update(content, pattern, sub): return re.sub(pattern, sub, content, flags=re.M | re.I) # Docs reference updates to current release version, for PyPI # This one gets the badge image content = content_update( content, r"(?<=/readthedocs/{0}/)\S+?(?=\.svg$)".format(NAME), "v" + new_ver ) # This one gets the RtD links content = content_update( content, r"(?<={0}\.readthedocs\.io/en/)\S+?(?=/)".format(NAME), "v" + new_ver ) return content setup( name=NAME, long_description=readme(), long_description_content_type="text/markdown", ) bskinn-stdio-mgr-8c8f709/src/000077500000000000000000000000001511250760100160405ustar00rootroot00000000000000bskinn-stdio-mgr-8c8f709/src/stdio_mgr/000077500000000000000000000000001511250760100200275ustar00rootroot00000000000000bskinn-stdio-mgr-8c8f709/src/stdio_mgr/__init__.py000066400000000000000000000012561511250760100221440ustar00rootroot00000000000000r"""``stdio_mgr`` *package definition module*. ``stdio_mgr`` provides a context manager for convenient mocking and/or wrapping of ``stdin``/``stdout``/``stderr`` interactions. **Author** Brian Skinn (bskinn@alum.mit.edu) **File Created** 24 Mar 2018 **Copyright** \(c) Brian Skinn 2018-2025 **Source Repository** http://www.github.com/bskinn/stdio-mgr **Documentation** See README.md at the GitHub repository **License** Code: `MIT License`_ Docs & Docstrings: |CC BY 4.0|_ See |license_txt|_ for full license terms. **Members** """ from stdio_mgr.stdio_mgr import stdio_mgr from stdio_mgr.version import __version__ __all__ = ["stdio_mgr"] bskinn-stdio-mgr-8c8f709/src/stdio_mgr/stdio_mgr.py000066400000000000000000000114651511250760100223770ustar00rootroot00000000000000r"""``stdio_mgr`` *code module*. ``stdio_mgr`` provides a context manager for convenient mocking and/or wrapping of ``stdin``/``stdout``/``stderr`` interactions. **Author** Brian Skinn (bskinn@alum.mit.edu) **File Created** 24 Mar 2018 **Copyright** \(c) Brian Skinn 2018-2025 **Source Repository** http://www.github.com/bskinn/stdio-mgr **Documentation** See README.md at the GitHub repository **License** Code: `MIT License`_ Docs & Docstrings: |CC BY 4.0|_ See |license_txt|_ for full license terms. **Members** """ import sys from contextlib import contextmanager from io import SEEK_END, SEEK_SET, StringIO, TextIOBase import attr @attr.s(slots=False) class TeeStdin(StringIO): """Class to tee contents to a side buffer on read. Subclass of :cls:`~io.StringIO` that overrides :meth:`~io.StringIO.read` and :meth:`~io.StringIO.readline` to tee all content *read* from the stream to `tee`. The canonical use-case is with :func:`stdio_mgr`, where `tee` is the mocked stream for `stdin`. To emphasize: teeing occurs on content *read*, **not write**. This class also provides the method :meth:`TeeStdin.append`, which is not available for the base :cls:`~io.StringIO` type. This method adds new content to the end of the stream while leaving the read position unchanged. Instantiation takes two arguments: `tee` :cls:`~io.TextIOBase` -- Text stream to receive content teed from :cls:`TeeStdin` upon read `init_text` |str| *(optional)* -- Text to use as the initial contents of the underlying :cls:`~io.StringIO`. `init_text` is passed directly to the :cls:~io.StringIO` instantiation call. Default is an empty |str|. """ tee = attr.ib(validator=attr.validators.instance_of(TextIOBase)) init_text = attr.ib(default="", validator=attr.validators.instance_of(str)) def __attrs_post_init__(self): """Call normal __init__ on superclass.""" super().__init__(self.init_text) def read(self, size=None): # pragma: no cover """Tee text to side buffer when read. Overrides :meth:`io.StringIO.read ` to implement the teeing. Parameters ---------- size |int| or |None| *(optional)* -- Number of characters to return; a negative or |None| value reads to EOF. """ text = super().read(size) self.tee.write(text) return text def readline(self, size=-1): """Tee text to side buffer when read. Overrides :meth:`io.StringIO.readline ` to implement the teeing. Parameters ---------- size |int| *(optional)* -- Number of characters to return; a negative value reads an entire line, regardless of length """ text = super().readline(size) self.tee.write(text) return text def append(self, text): """Write to end of stream while maintaining seek position. Actually stores the current position; seeks to end; writes `text`; and seeks to prior position. Parameters ---------- text |str| -- Text to append to the current stream contents. """ pos = self.tell() self.seek(0, SEEK_END) retval = self.write(text) self.seek(pos, SEEK_SET) return retval @contextmanager def stdio_mgr(in_str=""): r"""Subsitute temporary text buffers for `stdio` in a managed context. Context manager. Substitutes empty :cls:`~io.StringIO`\ s for :cls:`sys.stdout` and :cls:`sys.stderr`, and a :cls:`TeeStdin` for :cls:`sys.stdin` within the managed context. Upon exiting the context, the original stream objects are restored within :mod:`sys`, and the temporary streams are closed. Parameters ---------- in_str |str| *(optional)* -- Initialization text for the :cls:`TeeStdin` substitution for `stdin`. Default is an empty string. Yields ------ in_ :cls:`TeeStdin` -- Temporary stream for `stdin`. out_ :cls:`~io.StringIO` -- Temporary stream for `stdout`, initially empty. err_ :cls:`~io.StringIO` -- Temporary stream for `stderr`, initially empty. """ old_stdin = sys.stdin old_stdout = sys.stdout old_stderr = sys.stderr new_stdout = StringIO() new_stderr = StringIO() new_stdin = TeeStdin(new_stdout, in_str) sys.stdin = new_stdin sys.stdout = new_stdout sys.stderr = new_stderr yield new_stdin, new_stdout, new_stderr sys.stdin = old_stdin sys.stdout = old_stdout sys.stderr = old_stderr new_stdin.close() new_stdout.close() new_stderr.close() bskinn-stdio-mgr-8c8f709/src/stdio_mgr/version.py000066400000000000000000000011311511250760100220620ustar00rootroot00000000000000r"""``stdio_mgr`` *version definition module*. ``stdio_mgr`` provides a context manager for convenient mocking and/or wrapping of ``stdin``/``stdout``/``stderr`` interactions. **Author** Brian Skinn (bskinn@alum.mit.edu) **File Created** 28 Nov 2025 **Copyright** \(c) Brian Skinn 2018-2025 **Source Repository** http://www.github.com/bskinn/stdio-mgr **Documentation** See README.md at the GitHub repository **License** Code: `MIT License`_ Docs & Docstrings: |CC BY 4.0|_ See |license_txt|_ for full license terms. **Members** """ __version__ = "1.0.1.1" bskinn-stdio-mgr-8c8f709/tests/000077500000000000000000000000001511250760100164135ustar00rootroot00000000000000bskinn-stdio-mgr-8c8f709/tests/test_stdiomgr_base.py000066400000000000000000000053041511250760100226500ustar00rootroot00000000000000r"""*Base submodule for the* ``stdio_mgr`` *test suite*. ``stdio_mgr`` provides a context manager for convenient mocking and/or wrapping of ``stdin``/``stdout``/``stderr`` interactions. **Author** Brian Skinn (bskinn@alum.mit.edu) **File Created** 24 Mar 2018 **Copyright** \(c) Brian Skinn 2018-2025 **Source Repository** http://www.github.com/bskinn/stdio-mgr **Documentation** See README.md at the GitHub repository **License** Code: `MIT License`_ Docs & Docstrings: |CC BY 4.0|_ See |license_txt|_ for full license terms. **Members** """ import warnings from stdio_mgr import stdio_mgr def test_CaptureStdout(): # noqa: N802 """Confirm stdout capture.""" with stdio_mgr() as (i, o, e): s = "test str" print(s) # 'print' automatically adds a newline assert s + "\n" == o.getvalue() def test_CaptureStderr(): # noqa: N802 """Confirm stderr capture.""" with stdio_mgr() as (i, o, e): w = "This is a warning" warnings.warn(w, stacklevel=2) # Warning text comes at the end of a line; newline gets added assert w + "\n" in e.getvalue() def test_DefaultStdin(): # noqa: N802 """Confirm stdin default-populate.""" in_str = "This is a test string.\n" with stdio_mgr(in_str) as (i, o, e): assert in_str == i.getvalue() out_str = input() # TeeStdin tees the stream contents, *including* the newline, # to the managed stdout assert in_str == o.getvalue() # 'input' strips the trailing newline before returning assert in_str[:-1] == out_str def test_ManagedStdin(): # noqa: N802 """Confirm stdin populate within context.""" str1 = "This is a test string." str2 = "This is another test string.\n" with stdio_mgr() as (i, o, e): # Preload str1 to stdout, and check. As above, 'print' # appends a newline print(str1) assert str1 + "\n" == o.getvalue() # Use custom method .append to add the contents # without moving the seek position; check stdin contents. # The newline remains, since the stream contents were not # run through 'input' i.append(str2) assert str2 == i.getvalue() # Pull the contents of stdin to variable out_str = input() # stdout should have both strings. The newline of str2 is # *retained* here, because str2 was teed from stdin upon # the read of stdin by the above 'input' call. assert str1 + "\n" + str2 == o.getvalue() # 'input' should just have put str2 to out_str, *without* # the trailing newline, per normal 'input' behavior. assert str2[:-1] == out_str bskinn-stdio-mgr-8c8f709/tox.ini000066400000000000000000000054511511250760100165710ustar00rootroot00000000000000[tox] minversion=2.0 isolated_build=True envlist= py3{10,11,12,13,14}-attrs_latest py312-attrs_{17_1,17_2,17_3,18_1,18_2} py312-attrs_{18,19,20,21,22,23,24}_x sdist_install [testenv] commands= pytest deps= pytest attrs_17_1: attrs==17.1 attrs_17_2: attrs==17.2 attrs_17_3: attrs==17.3 attrs_17_4: attrs==17.4 attrs_18_1: attrs==18.1 attrs_18_2: attrs==18.2 attrs_18_x: attrs<19 attrs_19_x: attrs<20 attrs_20_x: attrs<21 attrs_21_x: attrs<22 attrs_22_x: attrs<23 attrs_23_x: attrs<24 attrs_24_x: attrs<25 attrs_latest: attrs [testenv:win] platform=win basepython= py313: python3.13 py312: python3.12 py311: python3.11 py310: python3.10 [testenv:linux] platform=linux basepython= py314: python3.14 py313: python3.13 py312: python3.12 py311: python3.11 py310: python3.10 [testenv:sdist_install] commands= python -c "import stdio_mgr" [testenv:flake8] skip_install=False deps= -rrequirements-flake8.txt commands= flake8 --version flake8 tests src [testenv:black] skip_install=True deps=black commands= black {posargs} . [testenv:isort] description=Sort, group, and coalesce imports skip_install=True deps=isort commands= isort --version isort {posargs} src tests [testenv:check] description=Run isort, black, and flake8 skip_install=True deps= isort black -r requirements-flake8.txt commands= isort --version isort {posargs} src tests black {posargs} . flake8 --version flake8 tests src [testenv:build] description=Build project to wheel and sdist skip_install=True deps=build commands= python -m build [pytest] # Disable pytest processing of warnings since we want to capture them in stderr # Must specify the README for doctesting addopts = -p no:warnings --doctest-glob="README.md" [flake8] # W503: black formats binary operators to start of line # RST30[56]: Ignore non-default substitutions/targets; use '$ make html O=-n' to find typos ignore = W503,RST305,RST306 show_source = True max_line_length = 88 # The 'bright black' (gray) requires ANSI escapes, which require literal ESC # characters when writing the escapes format = %(cyan)s%(path)s%(reset)s:%(yellow)s%(row)d%(reset)s:%(green)s%(col)d%(reset)s %(red)s(%(code)s)%(reset)s %(text)s per_file_ignores = # S101: pytest uses asserts liberally tests/*: S101 conftest.py: S101 # F401: MANY things imported but unused in __init__.py __init__.py: F401 # flake8-import-order import-order-style = smarkets application-import-names = stdio_mgr # flake8-rst-docstrings (requires >=0.0.11) # These declare directives/roles to be treated as 'known', # in addition to those in 'core' reST. rst-roles = attr,class,exc,func,meth,mod,obj,cls