diff --git a/.coveragerc b/.coveragerc index 4880d9b..4336ad9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,10 +1,10 @@ [run] -omit = - # Don't do coverage on test code + +include = + src/* tests/* - conftest.py - # Don't cover code in the env (Termux only?) +omit = env*/* [report] diff --git a/.github/workflows/all_core_tests.yml b/.github/workflows/all_core_tests.yml new file mode 100644 index 0000000..937bedc --- /dev/null +++ b/.github/workflows/all_core_tests.yml @@ -0,0 +1,77 @@ +name: '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 diff --git a/.github/workflows/release_platform_tests.yml b/.github/workflows/release_platform_tests.yml new file mode 100644 index 0000000..1ccb6f7 --- /dev/null +++ b/.github/workflows/release_platform_tests.yml @@ -0,0 +1,49 @@ +name: '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 diff --git a/.github/workflows/release_sdist_test.yml b/.github/workflows/release_sdist_test.yml new file mode 100644 index 0000000..09dd07f --- /dev/null +++ b/.github/workflows/release_sdist_test.yml @@ -0,0 +1,72 @@ +name: '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 diff --git a/.github/workflows/release_testdir_coverage.yml b/.github/workflows/release_testdir_coverage.yml new file mode 100644 index 0000000..94fffaf --- /dev/null +++ b/.github/workflows/release_testdir_coverage.yml @@ -0,0 +1,38 @@ +name: '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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e1e276b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -install: - - pip install -U --force-reinstall -r requirements-travis.txt - - is_py33=$( echo $TRAVIS_PYTHON_VERSION | grep -e '^3\.3' | wc -l ) - - if [ $is_py33 -gt 0 ]; then pip install 'setuptools==39.0.0'; fi - - pip install -e . -language: python -python: - - 3.3 - - 3.4 - - 3.5 - - 3.6 - - 3.7-dev -script: - - python --version - - pip list - - pytest --cov=src/stdio_mgr - - do_rest=$( echo $TRAVIS_PYTHON_VERSION | grep -e '^3\.6' | wc -l ) - - if [ $do_rest -gt 0 ]; then codecov; else echo "No codecov."; fi - - if [ $do_rest -gt 0 ]; then pip install black; black --check .; else echo "No black."; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ee248..c097aae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### [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 @@ -29,3 +47,5 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. which adds content to the end of the stream without changing the current seek position. + +[#108]: https://github.com/bskinn/stdio-mgr/pull/108 diff --git a/LICENSE.txt b/LICENSE.txt index 074cfdb..51d6dad 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2019 Brian Skinn +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 diff --git a/MANIFEST.in b/MANIFEST.in index de0a14a..9cc84b4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,4 @@ -include LICENSE.txt README.rst CHANGELOG.md pyproject.toml +include LICENSE.txt README.md CHANGELOG.md pyproject.toml +include requirements-dev.txt requirements-ci.txt requirements-flake8.txt +include tox.ini +include conftest.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..f82931d --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +## 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 diff --git a/README.rst b/README.rst deleted file mode 100644 index c030aa1..0000000 --- a/README.rst +++ /dev/null @@ -1,189 +0,0 @@ -stdio Manager: Context manager for mocking/wrapping ``stdin``/``stdout``/``stderr`` -=================================================================================== - -**Current Development Version:** - -.. image:: https://travis-ci.org/bskinn/stdio-mgr.svg?branch=dev - :target: https://travis-ci.org/bskinn/stdio-mgr - -.. image:: https://codecov.io/gh/bskinn/stdio-mgr/branch/dev/graph/badge.svg - :target: https://codecov.io/gh/bskinn/stdio-mgr - -**Most Recent Stable Release:** - -.. image:: https://img.shields.io/pypi/v/stdio_mgr.svg - :target: https://pypi.org/project/stdio-mgr - -.. image:: https://img.shields.io/pypi/pyversions/stdio-mgr.svg - -**Info:** - -.. image:: https://img.shields.io/github/license/mashape/apistatus.svg - :target: https://github.com/bskinn/stdio-mgr/blob/master/LICENSE.txt - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/ambv/black - ----- - -**Have a CLI Python application?** - -**Want to automate testing of the actual console input & output -of your user-facing components?** - -`stdio Manager` can help. - -While some functionality here is more or less duplicative of -``redirect_stdout`` and ``redirect_stderr`` in ``contextlib`` -`within the standard library `__, -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:** - -.. code:: - - $ pip install stdio-mgr - -Then use! - -All of the below examples assume ``stdio_mgr`` has already -been imported via: - -.. code:: - - from stdio_mgr import stdio_mgr - -**Mock** ``stdout``\ **:** - -.. code:: - - >>> 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 `__ -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``\ **:** - -.. code :: - - >>> 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: - -.. code:: - - >>> 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: - -.. code:: - - >>> 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 `__ -(``pip install stdio-mgr``). - -Source on `GitHub `__. Bug reports -and feature requests are welcomed at the -`Issues `__ page there. - -Copyright \(c) 2018-2019 Brian Skinn - -License: The MIT License. See `LICENSE.txt `__ -for full license terms. - diff --git a/conftest.py b/conftest.py index 70bcd56..1901fc5 100644 --- a/conftest.py +++ b/conftest.py @@ -11,13 +11,13 @@ 6 Feb 2019 **Copyright** - \(c) Brian Skinn 2018-2019 + \(c) Brian Skinn 2018-2025 **Source Repository** http://www.github.com/bskinn/stdio-mgr **Documentation** - See README.rst at the GitHub repository + See README.md at the GitHub repository **License** The MIT License; see |license_txt|_ for full license terms diff --git a/pyproject.toml b/pyproject.toml index 68780f8..f418b0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,76 @@ [build-system] -requires = ["wheel", "setuptools", "attrs>=17.1"] +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 = 79 +line-length = 88 include = ''' ( ^/tests/ diff --git a/requirements-ci.txt b/requirements-ci.txt new file mode 100644 index 0000000..e0bd0f0 --- /dev/null +++ b/requirements-ci.txt @@ -0,0 +1,5 @@ +attrs>=17 +pytest +pytest-cov +tox +-e . diff --git a/requirements-dev.txt b/requirements-dev.txt index a51dfa6..ba1426b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,4 @@ attrs>=17 -black -flake8==3.4.1 -flake8-docstrings==1.1.0 ipython pytest pytest-cov @@ -9,4 +6,4 @@ restview tox twine wget --e . \ No newline at end of file +-e . diff --git a/requirements-flake8.txt b/requirements-flake8.txt new file mode 100644 index 0000000..5d40486 --- /dev/null +++ b/requirements-flake8.txt @@ -0,0 +1,14 @@ +flake8>=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 diff --git a/requirements-travis.txt b/requirements-travis.txt deleted file mode 100644 index 49968d6..0000000 --- a/requirements-travis.txt +++ /dev/null @@ -1,7 +0,0 @@ -attrs>=17 -codecov -flake8==3.4.1 -flake8-docstrings==1.1.0 -pytest -pytest-cov - diff --git a/setup.py b/setup.py index 5050682..344300d 100644 --- a/setup.py +++ b/setup.py @@ -1,47 +1,43 @@ -import os, sys -from setuptools import setup, find_packages +import re +from pathlib import Path +from typing import Any, cast +from setuptools import setup -sys.path.append(os.path.abspath("src")) -from stdio_mgr import __version__ +NAME = "stdio-mgr" -sys.path.pop() +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(): - with open("README.rst", "r") as f: - return f.read() + 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="stdio-mgr", - version=__version__, - packages=find_packages("src"), - package_dir={"": "src"}, - provides=["stdio_mgr"], - requires=["attrs (>=17.1)"], - install_requires=["attrs>=17.1"], - python_requires=">=3", - url="https://www.github.com/bskinn/stdio-mgr", - license="MIT License", - author="Brian Skinn", - author_email="bskinn@alum.mit.edu", - description="Context manager for mocking/wrapping stdin/stdout/stderr", + name=NAME, long_description=readme(), - classifiers=[ - "License :: OSI Approved :: MIT License", - "Natural Language :: English", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Testing", - "Development Status :: 5 - Production/Stable", - ], + long_description_content_type="text/markdown", ) diff --git a/src/stdio_mgr/__init__.py b/src/stdio_mgr/__init__.py index 937f3a6..c7a4358 100644 --- a/src/stdio_mgr/__init__.py +++ b/src/stdio_mgr/__init__.py @@ -11,25 +11,26 @@ 24 Mar 2018 **Copyright** - \(c) Brian Skinn 2018-2019 + \(c) Brian Skinn 2018-2025 **Source Repository** http://www.github.com/bskinn/stdio-mgr **Documentation** - See README.rst at the GitHub repository + See README.md at the GitHub repository **License** - The MIT License; see |license_txt|_ for full license terms + 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"] - -from .stdio_mgr import stdio_mgr - - -__version__ = "1.0.1" diff --git a/src/stdio_mgr/stdio_mgr.py b/src/stdio_mgr/stdio_mgr.py index a4a8616..e94046e 100644 --- a/src/stdio_mgr/stdio_mgr.py +++ b/src/stdio_mgr/stdio_mgr.py @@ -11,23 +11,28 @@ 24 Mar 2018 **Copyright** - \(c) Brian Skinn 2018-2019 + \(c) Brian Skinn 2018-2025 **Source Repository** http://www.github.com/bskinn/stdio-mgr **Documentation** - See README.rst at the GitHub repository + See README.md at the GitHub repository **License** - The MIT License; see |license_txt|_ for full license terms + 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 StringIO, TextIOBase +from io import SEEK_END, SEEK_SET, StringIO, TextIOBase import attr @@ -67,8 +72,6 @@ class TeeStdin(StringIO): """ - from io import SEEK_SET, SEEK_END - tee = attr.ib(validator=attr.validators.instance_of(TextIOBase)) init_text = attr.ib(default="", validator=attr.validators.instance_of(str)) @@ -128,9 +131,9 @@ def append(self, text): """ pos = self.tell() - self.seek(0, self.SEEK_END) + self.seek(0, SEEK_END) retval = self.write(text) - self.seek(pos, self.SEEK_SET) + self.seek(pos, SEEK_SET) return retval @@ -172,8 +175,6 @@ def stdio_mgr(in_str=""): initially empty. """ - import sys - old_stdin = sys.stdin old_stdout = sys.stdout old_stderr = sys.stderr @@ -195,7 +196,3 @@ def stdio_mgr(in_str=""): new_stdin.close() new_stdout.close() new_stderr.close() - - -if __name__ == "__main__": # pragma: no cover - print("Module not executable.") diff --git a/src/stdio_mgr/version.py b/src/stdio_mgr/version.py new file mode 100644 index 0000000..5da15fc --- /dev/null +++ b/src/stdio_mgr/version.py @@ -0,0 +1,33 @@ +r"""``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" diff --git a/tests/test_stdiomgr_base.py b/tests/test_stdiomgr_base.py index 799d2fa..e7ef36f 100644 --- a/tests/test_stdiomgr_base.py +++ b/tests/test_stdiomgr_base.py @@ -11,26 +11,32 @@ 24 Mar 2018 **Copyright** - \(c) Brian Skinn 2018-2019 + \(c) Brian Skinn 2018-2025 **Source Repository** http://www.github.com/bskinn/stdio-mgr **Documentation** - See README.rst at the GitHub repository + See README.md at the GitHub repository **License** - The MIT License; see |license_txt|_ for full license terms + Code: `MIT License`_ + + Docs & Docstrings: |CC BY 4.0|_ + + See |license_txt|_ for full license terms. **Members** """ +import warnings -def test_CaptureStdout(): - """Confirm stdout capture.""" - from stdio_mgr import stdio_mgr +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) @@ -39,23 +45,18 @@ def test_CaptureStdout(): assert s + "\n" == o.getvalue() -def test_CaptureStderr(): +def test_CaptureStderr(): # noqa: N802 """Confirm stderr capture.""" - import warnings - from stdio_mgr import stdio_mgr - with stdio_mgr() as (i, o, e): w = "This is a warning" - warnings.warn(w) + 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(): +def test_DefaultStdin(): # noqa: N802 """Confirm stdin default-populate.""" - from stdio_mgr import stdio_mgr - in_str = "This is a test string.\n" with stdio_mgr(in_str) as (i, o, e): @@ -71,10 +72,8 @@ def test_DefaultStdin(): assert in_str[:-1] == out_str -def test_ManagedStdin(): +def test_ManagedStdin(): # noqa: N802 """Confirm stdin populate within context.""" - from stdio_mgr import stdio_mgr - str1 = "This is a test string." str2 = "This is another test string.\n" @@ -102,7 +101,3 @@ def test_ManagedStdin(): # 'input' should just have put str2 to out_str, *without* # the trailing newline, per normal 'input' behavior. assert str2[:-1] == out_str - - -if __name__ == "__main__": - print("Module not executable.") diff --git a/tox.ini b/tox.ini index d5bb123..99c671e 100644 --- a/tox.ini +++ b/tox.ini @@ -2,53 +2,123 @@ minversion=2.0 isolated_build=True envlist= - py3{3,4,5,6,7}-attrs_{17_4,18_2} - py36-attrs_{17_1,17_2,17_3,18_1,latest} - py33-attrs_17_3 - py37-attrs_latest + 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= - #python --version - #python tests.py -a 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 - attrs_17_1: pytest==3.2.5 - attrs_17_{2,3}: pytest==3.4.2 - attrs_17_4: pytest - attrs_18_{1,2}: pytest - attrs_latest: pytest - [testenv:win] platform=win basepython= - py37: C:\python37\python.exe - py36: C:\python36\python.exe - py35: C:\python35\python.exe - py34: C:\python34\python.exe - py33: C:\python33\python.exe + py313: python3.13 + py312: python3.12 + py311: python3.11 + py310: python3.10 [testenv:linux] platform=linux basepython= - py37: python3.7 - py36: python3.6 - py35: python3.5 - py34: python3.4 - py33: python3.3 + 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] -addopts = -p no:warnings --doctest-glob="README.rst" +# 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