Skip to content

Commit

Permalink
Provide a way to capture or wrap streams
Browse files Browse the repository at this point in the history
- Also fix a bug with error reporting during `_create_subprocess`
  failures
- Adds a stream wrapper using `TextIOWrapper` via
  `vistir.misc.StreamWrapper`
- Adds `vistir.misc.get_wrapped_stream()` function to wrap existing
  streams
- Adds `vistir.contextmanagers.replaced_stream()` to temporarily replace
  a stream
- Fixes #49
- Closes #48

Signed-off-by: Dan Ryan <dan@danryan.co>
  • Loading branch information
techalchemy committed Dec 9, 2018
1 parent 3c882ed commit 46c23cd
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 5 deletions.
121 changes: 120 additions & 1 deletion README.rst
Expand Up @@ -131,6 +131,7 @@ default encoding:
* ``vistir.contextmanagers.atomic_open_for_write``
* ``vistir.contextmanagers.cd``
* ``vistir.contextmanagers.open_file``
* ``vistir.contextmanagers.replaced_stream``
* ``vistir.contextmanagers.spinner``
* ``vistir.contextmanagers.temp_environ``
* ``vistir.contextmanagers.temp_path``
Expand Down Expand Up @@ -203,6 +204,23 @@ to pair this with an iterator which employs a sensible chunk size.
shutil.copyfileobj(fp, filecontents)
.. _`replaced_stream`:

A context manager to temporarily swap out *stream_name* with a stream wrapper. This will
capture the stream output and prevent it from being written as normal.

.. code-block:: python
>>> orig_stdout = sys.stdout
>>> with replaced_stream("stdout") as stdout:
... sys.stdout.write("hello")
... assert stdout.getvalue() == "hello"
... assert orig_stdout.getvalue() != "hello"
>>> sys.stdout.write("hello")
'hello'
.. _`spinner`:

**spinner**
Expand Down Expand Up @@ -286,8 +304,13 @@ The following Miscellaneous utilities are available as helper methods:
* ``vistir.misc.partialclass``
* ``vistir.misc.to_text``
* ``vistir.misc.to_bytes``
* ``vistir.misc.divide``
* ``vistir.misc.take``
* ``vistir.misc.chunked``
* ``vistir.misc.decode_for_output``

* ``vistir.misc.get_canonical_encoding_name``
* ``vistir.misc.get_wrapped_stream``
* ``vistir.misc.StreamWrapper``

.. _`shell_escape`:

Expand Down Expand Up @@ -401,6 +424,62 @@ Converts arbitrary byte-convertable input to bytes while handling errors.
b'this is some text'
.. _`chunked`:

**chunked**
////////////

Splits an iterable up into groups *of the specified length*, per `more itertools`_. Returns an iterable.

This example will create groups of chunk size **5**, which means there will be *6 groups*.

.. code-block:: python
>>> chunked_iterable = vistir.misc.chunked(5, range(30))
>>> for chunk in chunked_iterable:
... add_to_some_queue(chunk)
.. _more itertools: https://more-itertools.readthedocs.io/en/latest/api.html#grouping


.. _`take`:

**take**
/////////

Take elements from the supplied iterable without consuming it.

.. code-block:: python
>>> iterable = range(30)
>>> first_10 = take(10, iterable)
>>> [i for i in first_10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [i for i in iterable]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
.. _`divide`:

**divide**
////////////

Splits an iterable up into the *specified number of groups*, per `more itertools`_. Returns an iterable.

.. code-block:: python
>>> iterable = range(30)
>>> groups = []
>>> for grp in vistir.misc.divide(3, iterable):
... groups.append(grp)
>>> groups
[<tuple_iterator object at 0x7fb7966006a0>, <tuple_iterator object at 0x7fb796652780>, <tuple_iterator object at 0x7fb79650a2b0>]
.. _more itertools: https://more-itertools.readthedocs.io/en/latest/api.html#grouping


.. _`decode_for_output`:

**decode_for_output**
Expand All @@ -411,6 +490,46 @@ outputs using the system preferred locale using ``locale.getpreferredencoding(Fa
with some additional hackery on linux systems.


.. _`get_canonical_encoding_name`:

**get_canonical_encoding_name**
////////////////////////////////

Given an encoding name, get the canonical name from a codec lookup.

.. code-block:: python
>>> vistir.misc.get_canonical_encoding_name("utf8")
"utf-8"
.. _`get_wrapped_stream`:

**get_wrapped_stream**
//////////////////////

Given a stream, wrap it in a `StreamWrapper` instance and return the wrapped stream.

.. code-block:: python
>>> stream = sys.stdout
>>> wrapped_stream = vistir.misc.get_wrapped_stream(sys.stdout)
.. _`StreamWrapper`:

**StreamWrapper**
//////////////////

A stream wrapper and compatibility class for handling wrapping file-like stream objects
which may be used in place of ``sys.stdout`` and other streams.

.. code-block:: python
>>> wrapped_stream = vistir.misc.StreamWrapper(sys.stdout, encoding="utf-8", errors="replace", line_buffering=True)
>>> wrapped_stream = vistir.misc.StreamWrapper(io.StringIO(), encoding="utf-8", errors="replace", line_buffering=True)
🐉 Path Utilities
------------------

Expand Down
120 changes: 120 additions & 0 deletions docs/quickstart.rst
Expand Up @@ -131,6 +131,7 @@ defualt encoding:
* :func:`~vistir.contextmanagers.atomic_open_for_write`
* :func:`~vistir.contextmanagers.cd`
* :func:`~vistir.contextmanagers.open_file`
* :func:`~vistir.contextmanagers.replaced_stream`
* :func:`~vistir.contextmanagers.spinner`
* :func:`~vistir.contextmanagers.temp_environ`
* :func:`~vistir.contextmanagers.temp_path`
Expand Down Expand Up @@ -203,6 +204,23 @@ to pair this with an iterator which employs a sensible chunk size.
shutil.copyfileobj(fp, filecontents)
.. _`replaced_stream`:

A context manager to temporarily swap out *stream_name* with a stream wrapper. This will
capture the stream output and prevent it from being written as normal.

.. code-block:: python
>>> orig_stdout = sys.stdout
>>> with replaced_stream("stdout") as stdout:
... sys.stdout.write("hello")
... assert stdout.getvalue() == "hello"
... assert orig_stdout.getvalue() != "hello"
>>> sys.stdout.write("hello")
'hello'
.. _`spinner`:

**spinner**
Expand Down Expand Up @@ -286,7 +304,13 @@ The following Miscellaneous utilities are available as helper methods:
* :func:`~vistir.misc.partialclass`
* :func:`~vistir.misc.to_text`
* :func:`~vistir.misc.to_bytes`
* :func:`~vistir.misc.divide`
* :func:`~vistir.misc.take`
* :func:`~vistir.misc.chunked`
* :func:`~vistir.misc.decode_for_output`
* :func:`~vistir.misc.get_canonical_encoding_name`
* :func:`~vistir.misc.get_wrapped_stream`
* :class:`~vistir.misc.StreamWrapper`


.. _`shell_escape`:
Expand Down Expand Up @@ -401,6 +425,62 @@ Converts arbitrary byte-convertable input to bytes while handling errors.
b'this is some text'
.. _`chunked`:

**chunked**
////////////

Splits an iterable up into groups *of the specified length*, per `more itertools`_. Returns an iterable.

This example will create groups of chunk size **5**, which means there will be *6 groups*.

.. code-block:: python
>>> chunked_iterable = vistir.misc.chunked(5, range(30))
>>> for chunk in chunked_iterable:
... add_to_some_queue(chunk)
.. _more itertools: https://more-itertools.readthedocs.io/en/latest/api.html#grouping


.. _`take`:

**take**
/////////

Take elements from the supplied iterable without consuming it.

.. code-block:: python
>>> iterable = range(30)
>>> first_10 = take(10, iterable)
>>> [i for i in first_10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [i for i in iterable]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
.. _`divide`:

**divide**
////////////

Splits an iterable up into the *specified number of groups*, per `more itertools`_. Returns an iterable.

.. code-block:: python
>>> iterable = range(30)
>>> groups = []
>>> for grp in vistir.misc.divide(3, iterable):
... groups.append(grp)
>>> groups
[<tuple_iterator object at 0x7fb7966006a0>, <tuple_iterator object at 0x7fb796652780>, <tuple_iterator object at 0x7fb79650a2b0>]
.. _more itertools: https://more-itertools.readthedocs.io/en/latest/api.html#grouping


.. _`decode_for_output`:

**decode_for_output**
Expand All @@ -416,6 +496,46 @@ with some additional hackery on linux systems.
"some default locale encoded text"
.. _`get_canonical_encoding_name`:

**get_canonical_encoding_name**
////////////////////////////////

Given an encoding name, get the canonical name from a codec lookup.

.. code-block:: python
>>> vistir.misc.get_canonical_encoding_name("utf8")
"utf-8"
.. _`get_wrapped_stream`:

**get_wrapped_stream**
//////////////////////

Given a stream, wrap it in a `StreamWrapper` instance and return the wrapped stream.

.. code-block:: python
>>> stream = sys.stdout
>>> wrapped_stream = vistir.misc.get_wrapped_stream(sys.stdout)
.. _`StreamWrapper`:

**StreamWrapper**
//////////////////

A stream wrapper and compatibility class for handling wrapping file-like stream objects
which may be used in place of ``sys.stdout`` and other streams.

.. code-block:: python
>>> wrapped_stream = vistir.misc.StreamWrapper(sys.stdout, encoding="utf-8", errors="replace", line_buffering=True)
>>> wrapped_stream = vistir.misc.StreamWrapper(io.StringIO(), encoding="utf-8", errors="replace", line_buffering=True)
🐉 Path Utilities
------------------

Expand Down
3 changes: 3 additions & 0 deletions news/48.feature.rst
@@ -0,0 +1,3 @@
Added a new ``vistir.misc.StreamWrapper`` class with ``vistir.misc.get_wrapped_stream()`` to wrap existing streams
and ``vistir.contextmanagers.replaced_stream()`` to temporarily replace a stream.

1 change: 1 addition & 0 deletions news/49.bugfix.rst
@@ -0,0 +1 @@
Fixed a bug with exception handling during ``_create_process`` calls.
2 changes: 1 addition & 1 deletion setup.cfg
Expand Up @@ -34,7 +34,7 @@ python_requires = >=2.6,!=3.0,!=3.1,!=3.2,!=3.3
setup_requires = setuptools>=36.2.2
install_requires =
pathlib2;python_version<"3.5"
backports.functools_lru_cache;python_version <= "3.4"
backports.functools_lru_cache;python_version<="3.4"
backports.shutil_get_terminal_size;python_version<"3.3"
backports.weakref;python_version<"3.3"
requests
Expand Down
40 changes: 38 additions & 2 deletions src/vistir/contextmanagers.py
@@ -1,5 +1,5 @@
# -*- coding=utf-8 -*-
from __future__ import absolute_import, unicode_literals
from __future__ import absolute_import, unicode_literals, print_function

import io
import os
Expand All @@ -13,9 +13,14 @@
from .compat import NamedTemporaryFile, Path
from .path import is_file_url, is_valid_url, path_to_url, url_to_path

if six.PY2:
from io import BytesIO as StringIO
else:
from io import StringIO


__all__ = [
"temp_environ", "temp_path", "cd", "atomic_open_for_write", "open_file", "spinner"
"temp_environ", "temp_path", "cd", "atomic_open_for_write", "open_file", "spinner", "dummy_spinner",
]


Expand Down Expand Up @@ -286,3 +291,34 @@ def open_file(link, session=None, stream=True):
if conn is not None:
conn.close()
result.close()


@contextmanager
def replaced_stream(stream_name):
"""
Context manager to temporarily swap out *stream_name* with a stream wrapper.
:param str stream_name: The name of a sys stream to wrap
:returns: A ``StreamWrapper`` replacement, temporarily
>>> orig_stdout = sys.stdout
>>> with replaced_stream("stdout") as stdout:
... sys.stdout.write("hello")
... assert stdout.getvalue() == "hello"
... assert orig_stdout.getvalue() != "hello"
>>> sys.stdout.write("hello")
'hello'
"""
from .misc import StreamWrapper, get_canonical_encoding_name, PREFERRED_ENCODING
orig_stream = getattr(sys, stream_name)
encoding = get_canonical_encoding_name(
getattr(orig_stream, encoding, PREFERRED_ENCODING)
)
new_stream = StringIO()
wrapped_stream = StreamWrapper(new_stream, encoding, "replace", line_buffering=True)
try:
setattr(sys, stream_name, wrapped_stream)
yield getattr(sys, stream_name)
finally:
setattr(sys, stream_name, orig_stream)

0 comments on commit 46c23cd

Please sign in to comment.