Release 0.0.13.
[third_party/subunit] / python / subunit / __init__.py
index c997296467ab86b6dcb2abcd385a82871cbe69d2..5c33e62912a69db7629846fa78a4747aca8f32b4 100644 (file)
@@ -59,12 +59,12 @@ and newer).
 The ``tags(new_tags, gone_tags)`` method is called (if present) to add or
 remove tags in the test run that is currently executing. If called when no
 test is in progress (that is, if called outside of the ``startTest``,
-``stopTest`` pair), the the tags apply to all sebsequent tests. If called
+``stopTest`` pair), the the tags apply to all subsequent tests. If called
 when a test is in progress, then the tags only apply to that test.
 
 The ``time(a_datetime)`` method is called (if present) when a ``time:``
 directive is encountered in a Subunit stream. This is used to tell a TestResult
-about the time that events in the stream occured at, to allow reconstructing
+about the time that events in the stream occurred at, to allow reconstructing
 test timing from a stream.
 
 The ``progress(offset, whence)`` method controls progress data for a stream.
@@ -121,20 +121,46 @@ import re
 import subprocess
 import sys
 import unittest
+try:
+    from io import UnsupportedOperation as _UnsupportedOperation
+except ImportError:
+    _UnsupportedOperation = AttributeError
 
+from extras import safe_hasattr
 from testtools import content, content_type, ExtendedToOriginalDecorator
+from testtools.content import TracebackContent
 from testtools.compat import _b, _u, BytesIO, StringIO
 try:
     from testtools.testresult.real import _StringException
     RemoteException = _StringException
-    _remote_exception_str = '_StringException' # For testing.
+    # For testing: different pythons have different str() implementations.
+    if sys.version_info > (3, 0):
+        _remote_exception_str = "testtools.testresult.real._StringException"
+        _remote_exception_str_chunked = "34\r\n" + _remote_exception_str
+    else:
+        _remote_exception_str = "_StringException" 
+        _remote_exception_str_chunked = "1A\r\n" + _remote_exception_str
 except ImportError:
     raise ImportError ("testtools.testresult.real does not contain "
         "_StringException, check your version.")
-from testtools import testresult
+from testtools import testresult, CopyStreamResult
 
 from subunit import chunked, details, iso8601, test_results
+from subunit.v2 import ByteStreamToStreamResult, StreamResultToBytes
+
+# same format as sys.version_info: "A tuple containing the five components of
+# the version number: major, minor, micro, releaselevel, and serial. All
+# values except releaselevel are integers; the release level is 'alpha',
+# 'beta', 'candidate', or 'final'. The version_info value corresponding to the
+# Python version 2.0 is (2, 0, 0, 'final', 0)."  Additionally we use a
+# releaselevel of 'dev' for unreleased under-development code.
+#
+# If the releaselevel is 'alpha' then the major/minor/micro components are not
+# established at this point, and setup.py will use a version of next-$(revno).
+# If the releaselevel is 'final', then the tarball will be major.minor.micro.
+# Otherwise it is major.minor.micro~$(revno).
 
+__version__ = (0, 0, 13, 'final', 0)
 
 PROGRESS_SET = 0
 PROGRESS_CUR = 1
@@ -176,9 +202,15 @@ def tags_to_new_gone(tags):
 class DiscardStream(object):
     """A filelike object which discards what is written to it."""
 
+    def fileno(self):
+        raise _UnsupportedOperation()
+
     def write(self, bytes):
         pass
 
+    def read(self, len=0):
+        return _b('')
+
 
 class _ParserState(object):
     """State for the subunit parser."""
@@ -195,6 +227,7 @@ class _ParserState(object):
         self._tags_sym = (_b('tags'),)
         self._time_sym = (_b('time'),)
         self._xfail_sym = (_b('xfail'),)
+        self._uxsuccess_sym = (_b('uxsuccess'),)
         self._start_simple = _u(" [")
         self._start_multipart = _u(" [ multipart")
 
@@ -245,6 +278,8 @@ class _ParserState(object):
                 self.parser.subunitLineReceived(line)
             elif cmd in self._xfail_sym:
                 self.addExpectedFail(offset, line)
+            elif cmd in self._uxsuccess_sym:
+                self.addUnexpectedSuccess(offset, line)
             else:
                 self.parser.stdOutLineReceived(line)
         else:
@@ -308,6 +343,14 @@ class _InTest(_ParserState):
         self._outcome(offset, line, self._xfail,
             self.parser._reading_xfail_details)
 
+    def _uxsuccess(self):
+        self.parser.client.addUnexpectedSuccess(self.parser._current_test)
+
+    def addUnexpectedSuccess(self, offset, line):
+        """A 'uxsuccess:' directive has been read."""
+        self._outcome(offset, line, self._uxsuccess,
+            self.parser._reading_uxsuccess_details)
+
     def _failure(self):
         self.parser.client.addFailure(self.parser._current_test, details={})
 
@@ -419,6 +462,17 @@ class _ReadingExpectedFailureDetails(_ReadingDetails):
         return "xfail"
 
 
+class _ReadingUnexpectedSuccessDetails(_ReadingDetails):
+    """State for the subunit parser when reading uxsuccess details."""
+
+    def _report_outcome(self):
+        self.parser.client.addUnexpectedSuccess(self.parser._current_test,
+            details=self.details_parser.get_details())
+
+    def _outcome_label(self):
+        return "uxsuccess"
+
+
 class _ReadingSkipDetails(_ReadingDetails):
     """State for the subunit parser when reading skip details."""
 
@@ -462,10 +516,9 @@ class TestProtocolServer(object):
         """
         self.client = ExtendedToOriginalDecorator(client)
         if stream is None:
+            stream = sys.stdout
             if sys.version_info > (3, 0):
-                stream = sys.stdout.buffer
-            else:
-                stream = sys.stdout
+                stream = stream.buffer
         self._stream = stream
         self._forward_stream = forward_stream or DiscardStream()
         # state objects we can switch too
@@ -476,6 +529,7 @@ class TestProtocolServer(object):
         self._reading_skip_details = _ReadingSkipDetails(self)
         self._reading_success_details = _ReadingSuccessDetails(self)
         self._reading_xfail_details = _ReadingExpectedFailureDetails(self)
+        self._reading_uxsuccess_details = _ReadingUnexpectedSuccessDetails(self)
         # start with outside test.
         self._state = self._outside_test
         # Avoid casts on every call
@@ -502,7 +556,7 @@ class TestProtocolServer(object):
 
     def _handleTags(self, offset, line):
         """Process a tags command."""
-        tags = line[offset:].split()
+        tags = line[offset:].decode('utf8').split()
         new_tags, gone_tags = tags_to_new_gone(tags)
         self.client.tags(new_tags, gone_tags)
 
@@ -555,7 +609,8 @@ class TestProtocolClient(testresult.TestResult):
 
     # Get a TestSuite or TestCase to run
     suite = make_suite()
-    # Create a stream (any object with a 'write' method)
+    # Create a stream (any object with a 'write' method). This should accept
+    # bytes not strings: subunit is a byte orientated protocol.
     stream = file('tests.log', 'wb')
     # Create a subunit result object which will output to the stream
     result = subunit.TestProtocolClient(stream)
@@ -570,8 +625,16 @@ class TestProtocolClient(testresult.TestResult):
 
     def __init__(self, stream):
         testresult.TestResult.__init__(self)
+        stream = make_stream_binary(stream)
         self._stream = stream
-        _make_stream_binary(stream)
+        self._progress_fmt = _b("progress: ")
+        self._bytes_eol = _b("\n")
+        self._progress_plus = _b("+")
+        self._progress_push = _b("push")
+        self._progress_pop = _b("pop")
+        self._empty_bytes = _b("")
+        self._start_simple = _b(" [\n")
+        self._end_simple = _b("]\n")
 
     def addError(self, test, error=None, details=None):
         """Report an error in test test.
@@ -587,6 +650,8 @@ class TestProtocolClient(testresult.TestResult):
             to subunit.Content objects.
         """
         self._addOutcome("error", test, error=error, details=details)
+        if self.failfast:
+            self.stop()
 
     def addExpectedFailure(self, test, error=None, details=None):
         """Report an expected failure in test test.
@@ -617,8 +682,11 @@ class TestProtocolClient(testresult.TestResult):
             to subunit.Content objects.
         """
         self._addOutcome("failure", test, error=error, details=details)
+        if self.failfast:
+            self.stop()
 
-    def _addOutcome(self, outcome, test, error=None, details=None):
+    def _addOutcome(self, outcome, test, error=None, details=None,
+        error_permitted=True):
         """Report a failure in test test.
 
         Only one of error and details should be provided: conceptually there
@@ -632,43 +700,67 @@ class TestProtocolClient(testresult.TestResult):
             exc_info tuple.
         :param details: New Testing-in-python drafted API; a dict from string
             to subunit.Content objects.
-        """
-        self._stream.write("%s: %s" % (outcome, test.id()))
-        if error is None and details is None:
-            raise ValueError
-        if error is not None:
-            self._stream.write(" [\n")
-            # XXX: this needs to be made much stricter, along the lines of
-            # Martin[gz]'s work in testtools. Perhaps subunit can use that?
-            for line in self._exc_info_to_unicode(error, test).splitlines():
-                self._stream.write(("%s\n" % line).encode('utf8'))
+        :param error_permitted: If True then one and only one of error or
+            details must be supplied. If False then error must not be supplied
+            and details is still optional.  """
+        self._stream.write(_b("%s: " % outcome) + self._test_id(test))
+        if error_permitted:
+            if error is None and details is None:
+                raise ValueError
         else:
+            if error is not None:
+                raise ValueError
+        if error is not None:
+            self._stream.write(self._start_simple)
+            tb_content = TracebackContent(error, test)
+            for bytes in tb_content.iter_bytes():
+                self._stream.write(bytes)
+        elif details is not None:
             self._write_details(details)
-        self._stream.write("]\n")
+        else:
+            self._stream.write(_b("\n"))
+        if details is not None or error is not None:
+            self._stream.write(self._end_simple)
 
     def addSkip(self, test, reason=None, details=None):
         """Report a skipped test."""
         if reason is None:
             self._addOutcome("skip", test, error=None, details=details)
         else:
-            self._stream.write("skip: %s [\n" % test.id())
-            self._stream.write("%s\n" % reason)
-            self._stream.write("]\n")
+            self._stream.write(_b("skip: %s [\n" % test.id()))
+            self._stream.write(_b("%s\n" % reason))
+            self._stream.write(self._end_simple)
 
     def addSuccess(self, test, details=None):
         """Report a success in a test."""
-        self._stream.write("successful: %s" % test.id())
-        if not details:
-            self._stream.write("\n")
-        else:
-            self._write_details(details)
-            self._stream.write("]\n")
-    addUnexpectedSuccess = addSuccess
+        self._addOutcome("successful", test, details=details, error_permitted=False)
+
+    def addUnexpectedSuccess(self, test, details=None):
+        """Report an unexpected success in test test.
+
+        Details can optionally be provided: conceptually there
+        are two separate methods:
+            addError(self, test)
+            addError(self, test, details)
+
+        :param details: New Testing-in-python drafted API; a dict from string
+            to subunit.Content objects.
+        """
+        self._addOutcome("uxsuccess", test, details=details,
+            error_permitted=False)
+        if self.failfast:
+            self.stop()
+
+    def _test_id(self, test):
+        result = test.id()
+        if type(result) is not bytes:
+            result = result.encode('utf8')
+        return result
 
     def startTest(self, test):
         """Mark a test as starting its test run."""
         super(TestProtocolClient, self).startTest(test)
-        self._stream.write("test: %s\n" % test.id())
+        self._stream.write(_b("test: ") + self._test_id(test) + _b("\n"))
         self._stream.flush()
 
     def stopTest(self, test):
@@ -686,16 +778,28 @@ class TestProtocolClient(testresult.TestResult):
             PROGRESS_POP.
         """
         if whence == PROGRESS_CUR and offset > -1:
-            prefix = "+"
+            prefix = self._progress_plus
+            offset = _b(str(offset))
         elif whence == PROGRESS_PUSH:
-            prefix = ""
-            offset = "push"
+            prefix = self._empty_bytes
+            offset = self._progress_push
         elif whence == PROGRESS_POP:
-            prefix = ""
-            offset = "pop"
+            prefix = self._empty_bytes
+            offset = self._progress_pop
         else:
-            prefix = ""
-        self._stream.write("progress: %s%s\n" % (prefix, offset))
+            prefix = self._empty_bytes
+            offset = _b(str(offset))
+        self._stream.write(self._progress_fmt + prefix + offset +
+            self._bytes_eol)
+
+    def tags(self, new_tags, gone_tags):
+        """Inform the client about tags added/removed from the stream."""
+        if not new_tags and not gone_tags:
+            return
+        tags = set([tag.encode('utf8') for tag in new_tags])
+        tags.update([_b("-") + tag.encode('utf8') for tag in gone_tags])
+        tag_line = _b("tags: ") + _b(" ").join(tags) + _b("\n")
+        self._stream.write(tag_line)
 
     def time(self, a_datetime):
         """Inform the client of the time.
@@ -703,29 +807,29 @@ class TestProtocolClient(testresult.TestResult):
         ":param datetime: A datetime.datetime object.
         """
         time = a_datetime.astimezone(iso8601.Utc())
-        self._stream.write("time: %04d-%02d-%02d %02d:%02d:%02d.%06dZ\n" % (
+        self._stream.write(_b("time: %04d-%02d-%02d %02d:%02d:%02d.%06dZ\n" % (
             time.year, time.month, time.day, time.hour, time.minute,
-            time.second, time.microsecond))
+            time.second, time.microsecond)))
 
     def _write_details(self, details):
         """Output details to the stream.
 
         :param details: An extended details dict for a test outcome.
         """
-        self._stream.write(" [ multipart\n")
+        self._stream.write(_b(" [ multipart\n"))
         for name, content in sorted(details.items()):
-            self._stream.write("Content-Type: %s/%s" %
-                (content.content_type.type, content.content_type.subtype))
+            self._stream.write(_b("Content-Type: %s/%s" %
+                (content.content_type.type, content.content_type.subtype)))
             parameters = content.content_type.parameters
             if parameters:
-                self._stream.write(";")
+                self._stream.write(_b(";"))
                 param_strs = []
                 for param, value in parameters.items():
                     param_strs.append("%s=%s" % (param, value))
-                self._stream.write(",".join(param_strs))
-            self._stream.write("\n%s\n" % name)
+                self._stream.write(_b(",".join(param_strs)))
+            self._stream.write(_b("\n%s\n" % name))
             encoder = chunked.Encoder(self._stream)
-            map(encoder.write, content.iter_bytes())
+            list(map(encoder.write, content.iter_bytes()))
             encoder.close()
 
     def done(self):
@@ -816,8 +920,10 @@ class ExecTestCase(unittest.TestCase):
 
     def _run(self, result):
         protocol = TestProtocolServer(result)
-        output = subprocess.Popen(self.script, shell=True,
-            stdout=subprocess.PIPE).communicate()[0]
+        process = subprocess.Popen(self.script, shell=True,
+            stdout=subprocess.PIPE)
+        make_stream_binary(process.stdout)
+        output = process.communicate()[0]
         protocol.readFrom(BytesIO(output))
 
 
@@ -867,10 +973,10 @@ def run_isolated(klass, self, result):
         # at this point, sys.stdin is redirected, now we want
         # to filter it to escape ]'s.
         ### XXX: test and write that bit.
-
-        result = TestProtocolClient(sys.stdout)
+        stream = os.fdopen(1, 'wb')
+        result = TestProtocolClient(stream)
         klass.run(self, result)
-        sys.stdout.flush()
+        stream.flush()
         sys.stderr.flush()
         # exit HARD, exit NOW.
         os._exit(0)
@@ -880,50 +986,58 @@ def run_isolated(klass, self, result):
         os.close(c2pwrite)
         # hookup a protocol engine
         protocol = TestProtocolServer(result)
-        protocol.readFrom(os.fdopen(c2pread, 'rU'))
+        fileobj = os.fdopen(c2pread, 'rb')
+        protocol.readFrom(fileobj)
         os.waitpid(pid, 0)
         # TODO return code evaluation.
     return result
 
 
-def TAP2SubUnit(tap, subunit):
+def TAP2SubUnit(tap, output_stream):
     """Filter a TAP pipe into a subunit pipe.
 
-    :param tap: A tap pipe/stream/file object.
+    This should be invoked once per TAP script, as TAP scripts get
+    mapped to a single runnable case with multiple components.
+
+    :param tap: A tap pipe/stream/file object - should emit unicode strings.
     :param subunit: A pipe/stream/file object to write subunit results to.
     :return: The exit code to exit with.
     """
+    output = StreamResultToBytes(output_stream)
+    UTF8_TEXT = 'text/plain; charset=UTF8'
     BEFORE_PLAN = 0
     AFTER_PLAN = 1
     SKIP_STREAM = 2
     state = BEFORE_PLAN
     plan_start = 1
     plan_stop = 0
-    def _skipped_test(subunit, plan_start):
-        # Some tests were skipped.
-        subunit.write('test test %d\n' % plan_start)
-        subunit.write('error test %d [\n' % plan_start)
-        subunit.write('test missing from TAP output\n')
-        subunit.write(']\n')
-        return plan_start + 1
     # Test data for the next test to emit
     test_name = None
     log = []
     result = None
+    def missing_test(plan_start):
+        output.status(test_id='test %d' % plan_start,
+            test_status='fail', runnable=False, 
+            mime_type=UTF8_TEXT, eof=True, file_name="tap meta",
+            file_bytes=b"test missing from TAP output")
     def _emit_test():
         "write out a test"
         if test_name is None:
             return
-        subunit.write("test %s\n" % test_name)
-        if not log:
-            subunit.write("%s %s\n" % (result, test_name))
-        else:
-            subunit.write("%s %s [\n" % (result, test_name))
         if log:
-            for line in log:
-                subunit.write("%s\n" % line)
-            subunit.write("]\n")
+            log_bytes = b'\n'.join(log_line.encode('utf8') for log_line in log)
+            mime_type = UTF8_TEXT
+            file_name = 'tap comment'
+            eof = True
+        else:
+            log_bytes = None
+            mime_type = None
+            file_name = None
+            eof = True
         del log[:]
+        output.status(test_id=test_name, test_status=result,
+            file_bytes=log_bytes, mime_type=mime_type, eof=eof,
+            file_name=file_name, runnable=False)
     for line in tap:
         if state == BEFORE_PLAN:
             match = re.match("(\d+)\.\.(\d+)\s*(?:\#\s+(.*))?\n", line)
@@ -934,10 +1048,9 @@ def TAP2SubUnit(tap, subunit):
                 if plan_start > plan_stop and plan_stop == 0:
                     # skipped file
                     state = SKIP_STREAM
-                    subunit.write("test file skip\n")
-                    subunit.write("skip file skip [\n")
-                    subunit.write("%s\n" % comment)
-                    subunit.write("]\n")
+                    output.status(test_id='file skip', test_status='skip',
+                        file_bytes=comment.encode('utf8'), eof=True,
+                        file_name='tap comment')
                 continue
         # not a plan line, or have seen one before
         match = re.match("(ok|not ok)(?:\s+(\d+)?)?(?:\s+([^#]*[^#\s]+)\s*)?(?:\s+#\s+(TODO|SKIP|skip|todo)(?:\s+(.*))?)?\n", line)
@@ -948,7 +1061,7 @@ def TAP2SubUnit(tap, subunit):
             if status == 'ok':
                 result = 'success'
             else:
-                result = "failure"
+                result = "fail"
             if description is None:
                 description = ''
             else:
@@ -963,7 +1076,8 @@ def TAP2SubUnit(tap, subunit):
             if number is not None:
                 number = int(number)
                 while plan_start < number:
-                    plan_start = _skipped_test(subunit, plan_start)
+                    missing_test(plan_start)
+                    plan_start += 1
             test_name = "test %d%s" % (plan_start, description)
             plan_start += 1
             continue
@@ -976,18 +1090,21 @@ def TAP2SubUnit(tap, subunit):
                 extra = ' %s' % reason
             _emit_test()
             test_name = "Bail out!%s" % extra
-            result = "error"
+            result = "fail"
             state = SKIP_STREAM
             continue
         match = re.match("\#.*\n", line)
         if match:
             log.append(line[:-1])
             continue
-        subunit.write(line)
+        # Should look at buffering status and binding this to the prior result.
+        output.status(file_bytes=line.encode('utf8'), file_name='stdout',
+            mime_type=UTF8_TEXT)
     _emit_test()
     while plan_start <= plan_stop:
         # record missed tests
-        plan_start = _skipped_test(subunit, plan_start)
+        missing_test(plan_start)
+        plan_start += 1
     return 0
 
 
@@ -1015,24 +1132,21 @@ def tag_stream(original, filtered, tags):
     :return: 0
     """
     new_tags, gone_tags = tags_to_new_gone(tags)
-    def write_tags(new_tags, gone_tags):
-        if new_tags or gone_tags:
-            filtered.write("tags: " + ' '.join(new_tags))
-            if gone_tags:
-                for tag in gone_tags:
-                    filtered.write("-" + tag)
-            filtered.write("\n")
-    write_tags(new_tags, gone_tags)
-    # TODO: use the protocol parser and thus don't mangle test comments.
-    for line in original:
-        if line.startswith("tags:"):
-            line_tags = line[5:].split()
-            line_new, line_gone = tags_to_new_gone(line_tags)
-            line_new = line_new - gone_tags
-            line_gone = line_gone - new_tags
-            write_tags(line_new, line_gone)
-        else:
-            filtered.write(line)
+    source = ByteStreamToStreamResult(original, non_subunit_name='stdout')
+    class Tagger(CopyStreamResult):
+        def status(self, **kwargs):
+            tags = kwargs.get('test_tags')
+            if not tags:
+                tags = set()
+            tags.update(new_tags)
+            tags.difference_update(gone_tags)
+            if tags:
+                kwargs['test_tags'] = tags
+            else:
+                kwargs['test_tags'] = None
+            super(Tagger, self).status(**kwargs)
+    output = Tagger([StreamResultToBytes(filtered)])
+    source.run(output)
     return 0
 
 
@@ -1061,7 +1175,7 @@ class ProtocolTestCase(object):
     :seealso: TestProtocolServer (the subunit wire protocol parser).
     """
 
-    def __init__(self, stream, passthrough=None, forward=False):
+    def __init__(self, stream, passthrough=None, forward=None):
         """Create a ProtocolTestCase reading from stream.
 
         :param stream: A filelike object which a subunit stream can be read
@@ -1071,9 +1185,11 @@ class ProtocolTestCase(object):
         :param forward: A stream to pass subunit input on to. If not supplied
             subunit input is not forwarded.
         """
+        stream = make_stream_binary(stream)
         self._stream = stream
-        _make_stream_binary(stream)
         self._passthrough = passthrough
+        if forward is not None:
+            forward = make_stream_binary(forward)
         self._forward = forward
 
     def __call__(self, result=None):
@@ -1150,24 +1266,62 @@ def get_default_formatter():
     if formatter:
         return os.popen(formatter, "w")
     else:
-        return sys.stdout
+        stream = sys.stdout
+        if sys.version_info > (3, 0):
+            if safe_hasattr(stream, 'buffer'):
+                stream = stream.buffer
+        return stream
 
 
-if sys.version_info > (3, 0):
-    from io import UnsupportedOperation as _NoFilenoError
-else:
-    _NoFilenoError = AttributeError
+def read_test_list(path):
+    """Read a list of test ids from a file on disk.
 
-def _make_stream_binary(stream):
-    """Ensure that a stream will be binary safe. See _make_binary_on_windows."""
+    :param path: Path to the file
+    :return: Sequence of test ids
+    """
+    f = open(path, 'rb')
+    try:
+        return [l.rstrip("\n") for l in f.readlines()]
+    finally:
+        f.close()
+
+
+def make_stream_binary(stream):
+    """Ensure that a stream will be binary safe. See _make_binary_on_windows.
+    
+    :return: A binary version of the same stream (some streams cannot be
+        'fixed' but can be unwrapped).
+    """
     try:
         fileno = stream.fileno()
-    except _NoFilenoError:
-        return
-    _make_binary_on_windows(fileno)
+    except (_UnsupportedOperation, AttributeError):
+        pass
+    else:
+        _make_binary_on_windows(fileno)
+    return _unwrap_text(stream)
+
 
 def _make_binary_on_windows(fileno):
     """Win32 mangles \r\n to \n and that breaks streams. See bug lp:505078."""
     if sys.platform == "win32":
         import msvcrt
         msvcrt.setmode(fileno, os.O_BINARY)
+
+
+def _unwrap_text(stream):
+    """Unwrap stream if it is a text stream to get the original buffer."""
+    if sys.version_info > (3, 0):
+        unicode_type = str
+    else:
+        unicode_type = unicode
+    try:
+        # Read streams
+        if type(stream.read(0)) is unicode_type:
+            return stream.buffer
+    except (_UnsupportedOperation, IOError):
+        # Cannot read from the stream: try via writes
+        try:
+            stream.write(_b(''))
+        except TypeError:
+            return stream.buffer
+    return stream