From 523984ba06fe988e36e7acd700043bbcc4cf8da6 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Mon, 17 Dec 2012 01:18:12 +1300 Subject: * ``DetailStream`` was ambiguous about whether it handled bytes or characters, which matters a lot for Python3. It now is deprecated with ByteStream and StringStream replacing it. (Robert Collins) * Fixtures is now Python3 compatible. (Robert Collins) --- NEWS | 6 ++ lib/fixtures/__init__.py | 4 + lib/fixtures/_fixtures/__init__.py | 8 +- lib/fixtures/_fixtures/detailstream.py | 43 ---------- lib/fixtures/_fixtures/logger.py | 11 ++- lib/fixtures/_fixtures/streams.py | 95 +++++++++++++++++++++++ lib/fixtures/fixture.py | 2 +- lib/fixtures/tests/_fixtures/__init__.py | 2 +- lib/fixtures/tests/_fixtures/test_detailstream.py | 54 ------------- lib/fixtures/tests/_fixtures/test_logger.py | 19 ++++- lib/fixtures/tests/_fixtures/test_streams.py | 69 ++++++++++++++++ 11 files changed, 205 insertions(+), 108 deletions(-) delete mode 100644 lib/fixtures/_fixtures/detailstream.py create mode 100644 lib/fixtures/_fixtures/streams.py delete mode 100644 lib/fixtures/tests/_fixtures/test_detailstream.py create mode 100644 lib/fixtures/tests/_fixtures/test_streams.py diff --git a/NEWS b/NEWS index 4f82b24..a888fe1 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,12 @@ NEXT CHANGES ------- +* ``DetailStream`` was ambiguous about whether it handled bytes or characters, + which matters a lot for Python3. It now is deprecated with ByteStream and + StringStream replacing it. (Robert Collins) + +* Fixtures is now Python3 compatible. (Robert Collins) + * ``FakeLogger`` has been split out into a ``LogHandler`` fixture that can inject arbitrary handlers, giving more flexability. (Jonathan Lange) diff --git a/lib/fixtures/__init__.py b/lib/fixtures/__init__.py index 1390ac8..1c03bf8 100644 --- a/lib/fixtures/__init__.py +++ b/lib/fixtures/__init__.py @@ -39,6 +39,7 @@ Most users will want to look at TestWithFixtures and Fixture, to start with. __version__ = (0, 3, 10, 'final', 0) __all__ = [ + 'ByteStream', 'DetailStream', 'EnvironmentVariable', 'EnvironmentVariableFixture', @@ -55,6 +56,7 @@ __all__ = [ 'PopenFixture', 'PythonPackage', 'PythonPathEntry', + 'StringStream', 'TempDir', 'TempHomeDir', 'TestWithFixtures', @@ -69,6 +71,7 @@ from fixtures.fixture import ( MethodFixture, ) from fixtures._fixtures import ( + ByteStream, DetailStream, EnvironmentVariable, EnvironmentVariableFixture, @@ -82,6 +85,7 @@ from fixtures._fixtures import ( PopenFixture, PythonPackage, PythonPathEntry, + StringStream, TempDir, TempHomeDir, Timeout, diff --git a/lib/fixtures/_fixtures/__init__.py b/lib/fixtures/_fixtures/__init__.py index e113e1e..1d54858 100644 --- a/lib/fixtures/_fixtures/__init__.py +++ b/lib/fixtures/_fixtures/__init__.py @@ -17,6 +17,7 @@ """Included fixtures.""" __all__ = [ + 'ByteStream', 'DetailStream', 'EnvironmentVariable', 'EnvironmentVariableFixture', @@ -30,6 +31,7 @@ __all__ = [ 'PopenFixture', 'PythonPackage', 'PythonPathEntry', + 'StringStream', 'TempDir', 'TempHomeDir', 'Timeout', @@ -37,7 +39,6 @@ __all__ = [ ] -from fixtures._fixtures.detailstream import DetailStream from fixtures._fixtures.environ import ( EnvironmentVariable, EnvironmentVariableFixture, @@ -55,6 +56,11 @@ from fixtures._fixtures.popen import ( from fixtures._fixtures.packagepath import PackagePathEntry from fixtures._fixtures.pythonpackage import PythonPackage from fixtures._fixtures.pythonpath import PythonPathEntry +from fixtures._fixtures.streams import ( + ByteStream, + DetailStream, + StringStream, + ) from fixtures._fixtures.tempdir import ( NestedTempfile, TempDir, diff --git a/lib/fixtures/_fixtures/detailstream.py b/lib/fixtures/_fixtures/detailstream.py deleted file mode 100644 index 779ef18..0000000 --- a/lib/fixtures/_fixtures/detailstream.py +++ /dev/null @@ -1,43 +0,0 @@ -# fixtures: Fixtures with cleanups for testing and convenience. -# -# Copyright (c) 2012, Robert Collins -# -# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause -# license at the users choice. A copy of both licenses are available in the -# project source as Apache-2.0 and BSD. You may not use this file except in -# compliance with one of these two licences. -# -# Unless required by applicable law or agreed to in writing, software -# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# license you chose for the specific language governing permissions and -# limitations under that license. - -__all__ = [ - 'DetailStream' - ] - -from cStringIO import StringIO - -from fixtures import Fixture -import testtools - - -class DetailStream(Fixture): - """Provide a file-like object and expose it as a detail. - - :attr stream: The file-like object. - """ - - def __init__(self, detail_name): - """Create a DetailStream. - - :param detail_name: Use this as the name of the stream. - """ - self._detail_name = detail_name - - def setUp(self): - super(DetailStream, self).setUp() - self.stream = StringIO() - self.addDetail(self._detail_name, - testtools.content.content_from_stream(self.stream, seek_offset=0)) diff --git a/lib/fixtures/_fixtures/logger.py b/lib/fixtures/_fixtures/logger.py index 0beb170..e46de3a 100644 --- a/lib/fixtures/_fixtures/logger.py +++ b/lib/fixtures/_fixtures/logger.py @@ -15,8 +15,10 @@ from logging import StreamHandler, getLogger, INFO, Formatter +from testtools.compat import _u + from fixtures import Fixture -from fixtures._fixtures.detailstream import DetailStream +from fixtures._fixtures.streams import StringStream __all__ = [ 'FakeLogger', @@ -88,8 +90,8 @@ class FakeLogger(Fixture): def setUp(self): super(FakeLogger, self).setUp() - name = u"pythonlogging:'%s'" % self._name - output = self.useFixture(DetailStream(name)).stream + name = _u("pythonlogging:'%s'") % self._name + output = self.useFixture(StringStream(name)).stream self._output = output handler = StreamHandler(output) if self._format: @@ -100,7 +102,8 @@ class FakeLogger(Fixture): @property def output(self): - return self._output.getvalue() + self._output.seek(0) + return self._output.read() LoggerFixture = FakeLogger diff --git a/lib/fixtures/_fixtures/streams.py b/lib/fixtures/_fixtures/streams.py new file mode 100644 index 0000000..654ee67 --- /dev/null +++ b/lib/fixtures/_fixtures/streams.py @@ -0,0 +1,95 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2012, Robert Collins +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'ByteStream', + 'DetailStream', + 'StringStream', + ] + +import io +import sys + +from fixtures import Fixture +import testtools + + +class Stream(Fixture): + """Expose a file-like object as a detail. + + :attr stream: The file-like object. + """ + + def __init__(self, detail_name, stream_factory): + """Create a ByteStream. + + :param detail_name: Use this as the name of the stream. + :param stream_factory: Called to construct a pair of streams: + (write_stream, content_stream). + """ + self._detail_name = detail_name + self._stream_factory = stream_factory + + def setUp(self): + super(Stream, self).setUp() + write_stream, read_stream = self._stream_factory() + self.stream = write_stream + self.addDetail(self._detail_name, + testtools.content.content_from_stream(read_stream, seek_offset=0)) + + +def _byte_stream_factory(): + result = io.BytesIO() + return (result, result) + + +def ByteStream(detail_name): + """Provide a file-like object that accepts bytes and expose as a detail. + + :param detail_name: The name of the detail. + :return: A fixture which has an attribute `stream` containing the file-like + object. + """ + return Stream(detail_name, _byte_stream_factory) + + +def _string_stream_factory(): + lower = io.BytesIO() + upper = io.TextIOWrapper(lower, encoding="utf8") + # In theory, this is sufficient and correct, but on Python2, + # upper.write(_b('foo")) will whinge louadly. + if sys.version_info[0] < 3: + upper_write = upper.write + def safe_write(str_or_bytes): + if type(str_or_bytes) is str: + str_or_bytes = str_or_bytes.decode('utf8') + return upper_write(str_or_bytes) + upper.write = safe_write + return upper, lower + + +def StringStream(detail_name): + """Provide a file-like object that accepts strings and expose as a detail. + + :param detail_name: The name of the detail. + :return: A fixture which has an attribute `stream` containing the file-like + object. + """ + return Stream(detail_name, _string_stream_factory) + + +def DetailStream(detail_name): + """Deprecated alias for ByteStream.""" + return ByteStream(detail_name) diff --git a/lib/fixtures/fixture.py b/lib/fixtures/fixture.py index 6c3422f..2cf966d 100644 --- a/lib/fixtures/fixture.py +++ b/lib/fixtures/fixture.py @@ -29,7 +29,7 @@ from testtools.compat import ( ) from testtools.helpers import try_import -from callmany import ( +from fixtures.callmany import ( CallMany, # Deprecated, imported for compatibility. MultipleExceptions, diff --git a/lib/fixtures/tests/_fixtures/__init__.py b/lib/fixtures/tests/_fixtures/__init__.py index 38987b9..e4d9403 100644 --- a/lib/fixtures/tests/_fixtures/__init__.py +++ b/lib/fixtures/tests/_fixtures/__init__.py @@ -15,7 +15,6 @@ def load_tests(loader, standard_tests, pattern): test_modules = [ - 'detailstream', 'environ', 'logger', 'monkeypatch', @@ -23,6 +22,7 @@ def load_tests(loader, standard_tests, pattern): 'popen', 'pythonpackage', 'pythonpath', + 'streams', 'tempdir', 'temphomedir', 'timeout', diff --git a/lib/fixtures/tests/_fixtures/test_detailstream.py b/lib/fixtures/tests/_fixtures/test_detailstream.py deleted file mode 100644 index 9df351d..0000000 --- a/lib/fixtures/tests/_fixtures/test_detailstream.py +++ /dev/null @@ -1,54 +0,0 @@ -# fixtures: Fixtures with cleanups for testing and convenience. -# -# Copyright (c) 2012, Robert Collins -# -# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause -# license at the users choice. A copy of both licenses are available in the -# project source as Apache-2.0 and BSD. You may not use this file except in -# compliance with one of these two licences. -# -# Unless required by applicable law or agreed to in writing, software -# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# license you chose for the specific language governing permissions and -# limitations under that license. - -from testtools import TestCase - -from fixtures import DetailStream - - -class DetailStreamTest(TestCase): - - def test_empty_detail_stream(self): - detail_name = 'test' - fixture = DetailStream(detail_name) - with fixture: - content = fixture.getDetails()[detail_name] - self.assertEqual("", content.as_text()) - - def test_stream_content_in_details(self): - detail_name = 'test' - fixture = DetailStream(detail_name) - with fixture: - stream = fixture.stream - content = fixture.getDetails()[detail_name] - # Output after getDetails is called is included. - stream.write("testing 1 2 3") - self.assertEqual("testing 1 2 3", content.as_text()) - - def test_stream_content_reset(self): - detail_name = 'test' - fixture = DetailStream(detail_name) - with fixture: - stream = fixture.stream - content = fixture.getDetails()[detail_name] - stream.write("testing 1 2 3") - with fixture: - # The old content object returns the old usage - self.assertEqual("testing 1 2 3", content.as_text()) - content = fixture.getDetails()[detail_name] - # A new fixture returns the new output: - stream = fixture.stream - stream.write("1 2 3 testing") - self.assertEqual("1 2 3 testing", content.as_text()) diff --git a/lib/fixtures/tests/_fixtures/test_logger.py b/lib/fixtures/tests/_fixtures/test_logger.py index c05e862..1f69459 100644 --- a/lib/fixtures/tests/_fixtures/test_logger.py +++ b/lib/fixtures/tests/_fixtures/test_logger.py @@ -16,7 +16,7 @@ import logging from testtools import TestCase -from cStringIO import StringIO +from testtools.compat import StringIO from fixtures import ( FakeLogger, @@ -89,11 +89,22 @@ class FakeLoggerTest(TestCase, TestWithFixtures): # Output after getDetails is called is included. logging.info('some message') self.assertEqual("some message\n", content.as_text()) + # The old content object returns the old usage after cleanUp (not + # strictly needed but convenient). Note that no guarantee is made that + # it will work after setUp is called again. [It does on Python 2.x, not + # on 3.x] + self.assertEqual("some message\n", content.as_text()) with fixture: - # The old content object returns the old usage - self.assertEqual("some message\n", content.as_text()) - # A new one returns the new output: + # A new one returns new output: self.assertEqual("", fixture.getDetails()[detail_name].as_text()) + # The original content object may either fail, or return the old + # content (it must not have been reset..). + try: + self.assertEqual("some message\n", content.as_text()) + except AssertionError: + raise + except: + pass class LogHandlerTest(TestCase, TestWithFixtures): diff --git a/lib/fixtures/tests/_fixtures/test_streams.py b/lib/fixtures/tests/_fixtures/test_streams.py new file mode 100644 index 0000000..b4f4838 --- /dev/null +++ b/lib/fixtures/tests/_fixtures/test_streams.py @@ -0,0 +1,69 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2012, Robert Collins +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +from testtools import TestCase +from testtools.compat import ( + _b, + _u, + ) +from testtools.matchers import Contains + +from fixtures import ( + ByteStream, + DetailStream, + StringStream, + ) + + +class DetailStreamTest(TestCase): + + def test_doc_mentions_deprecated(self): + self.assertThat(DetailStream.__doc__, Contains('Deprecated')) + + +class TestByteStreams(TestCase): + + def test_empty_detail_stream(self): + detail_name = 'test' + fixture = DetailStream(detail_name) + with fixture: + content = fixture.getDetails()[detail_name] + self.assertEqual(_u(""), content.as_text()) + + def test_stream_content_in_details(self): + detail_name = 'test' + fixture = DetailStream(detail_name) + with fixture: + stream = fixture.stream + content = fixture.getDetails()[detail_name] + # Output after getDetails is called is included. + stream.write(_b("testing 1 2 3")) + self.assertEqual("testing 1 2 3", content.as_text()) + + def test_stream_content_reset(self): + detail_name = 'test' + fixture = DetailStream(detail_name) + with fixture: + stream = fixture.stream + content = fixture.getDetails()[detail_name] + stream.write(_b("testing 1 2 3")) + with fixture: + # The old content object returns the old usage + self.assertEqual(_u("testing 1 2 3"), content.as_text()) + content = fixture.getDetails()[detail_name] + # A new fixture returns the new output: + stream = fixture.stream + stream.write(_b("1 2 3 testing")) + self.assertEqual(_u("1 2 3 testing"), content.as_text()) -- cgit v1.2.1