diff --git a/README b/README index 3a9d594..d74af26 100644 --- a/README +++ b/README @@ -342,6 +342,15 @@ in test failure descriptions. Very useful in combination with MonkeyPatch. ... pass >>> fixture.cleanUp() +Deprecations +++++++++++++ + +Prevent code under test from calling deprecated functions. + + >>> import warnings + >>> with fixtures.Deprecations('my_module'): + ... warnings.warn('stop using me', DeprecationWarning) # will raise + EnvironmentVariable +++++++++++++++++++ diff --git a/fixtures/__init__.py b/fixtures/__init__.py index cef2af2..0c8fca5 100644 --- a/fixtures/__init__.py +++ b/fixtures/__init__.py @@ -45,6 +45,7 @@ __all__ = [ 'ByteStream', 'CompoundFixture', + 'Deprecations', 'DetailStream', 'EnvironmentVariable', 'EnvironmentVariableFixture', @@ -88,6 +89,7 @@ ) from fixtures._fixtures import ( ByteStream, + Deprecations, DetailStream, EnvironmentVariable, EnvironmentVariableFixture, diff --git a/fixtures/_fixtures/__init__.py b/fixtures/_fixtures/__init__.py index b60a6fb..d30c566 100644 --- a/fixtures/_fixtures/__init__.py +++ b/fixtures/_fixtures/__init__.py @@ -18,6 +18,7 @@ __all__ = [ 'ByteStream', + 'Deprecations', 'DetailStream', 'EnvironmentVariable', 'EnvironmentVariableFixture', @@ -43,6 +44,9 @@ ] +from fixtures._fixtures.deprecations import ( + Deprecations, + ) from fixtures._fixtures.environ import ( EnvironmentVariable, EnvironmentVariableFixture, diff --git a/fixtures/_fixtures/deprecations.py b/fixtures/_fixtures/deprecations.py new file mode 100644 index 0000000..9325b77 --- /dev/null +++ b/fixtures/_fixtures/deprecations.py @@ -0,0 +1,95 @@ +# Copyright (c) 2015 IBM Corp. +# +# 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 __future__ import absolute_import + +__all__ = [ + 'Deprecations', +] + +import contextlib +import re +import warnings # conflicts with the local warnings module so absolute_import + +import fixtures + + +class Deprecations(fixtures.Fixture): + """Prevent calls to deprecated functions. + + This fixture can be added to a testcase to ensure that the code under test + isn't calling deprecated function. It sets Python's `warnings` module for + the module under test to "error" so that DeprecationWarning will be + raised. + + You might want your application to not use any deprecated function. + Deprecated function is going to be removed and sometimes is being removed + because it's buggy and you shouldn't be using it. + + It can be difficult to tell just through code reviews that new code is + calling deprecated function. This fixture can be used to protect you from + developers proposing changes that use deprecated function. + + It can also be useful to be able to test if your application is still using + some function that's been newly deprecated. + + .. note:: This fixture uses :func:`warnings.catch_warnings`, as such the + note there applies: The fixture modifies global state and therefore is + not thread safe. + + :param str module: The name of a Python module. DeprecationWarnings emitted + from this module will cause an error to be raised. + """ + + def __init__(self, module): + super(Deprecations, self).__init__() + self._module_regex = '^%s' % re.escape(module + '.') + + def _setUp(self): + cw = warnings.catch_warnings() + cw.__enter__() + self.addCleanup(cw.__exit__) + warnings.filterwarnings('error', category=DeprecationWarning, + module=self._module_regex) + + def ignore_deprecations(self): + """Indicate that this test expects to call deprecated function. + + Normally you'll want to protect all tests from calling deprecated + functions, then some function is deprecated and now tests are failing + due to the deprecation. This function can be used to indicate + that the test is going to call deprecated function and not to fail. + This can be used as a marker for either tests that are there to verify + deprecated functions continue to work and will be removed along with + the function, or as tests that need to be fixed to stop calling + deprecated functions. + """ + warnings.filterwarnings('ignore', category=DeprecationWarning, + module=self._module_regex) + + @contextlib.contextmanager + def ignore_deprecations_here(self): + """This section of code ignores calls to deprecated functions. + + If you've got a test that part of it is testing deprecated functions + then wrap the part in this context manager:: + + with self.deprecations.expect_deprecations_here(): + call_deprecated_function() + + """ + self.cleanUp() + try: + yield + finally: + self.setUp() diff --git a/fixtures/tests/_fixtures/__init__.py b/fixtures/tests/_fixtures/__init__.py index 1b4c359..12347fc 100644 --- a/fixtures/tests/_fixtures/__init__.py +++ b/fixtures/tests/_fixtures/__init__.py @@ -15,6 +15,7 @@ def load_tests(loader, standard_tests, pattern): test_modules = [ + 'deprecations', 'environ', 'logger', 'mockpatch', diff --git a/fixtures/tests/_fixtures/test_deprecations.py b/fixtures/tests/_fixtures/test_deprecations.py new file mode 100644 index 0000000..83b1744 --- /dev/null +++ b/fixtures/tests/_fixtures/test_deprecations.py @@ -0,0 +1,72 @@ +# Copyright (c) 2015 IBM Corp. +# +# 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. + +import warnings + +import testtools + +from fixtures import Deprecations + + +MODULE = 'fixtures' + + +class TestDeprecations(testtools.TestCase): + def test_null_case(self): + # When the Deprecations fixture isn't used then deprecations are not + # errors. This shows that python works as required for these tests. + warnings.warn('message ignored', DeprecationWarning) + + def test_enabled_raises(self): + # When the Deprecations fixture is active, calling deprecated function + # is an error. + self.useFixture(Deprecations(MODULE)) + self.assertRaises( + DeprecationWarning, + lambda: warnings.warn('message ignored', DeprecationWarning)) + + def test_ignore_deprecations(self): + # When ignore_deprecations() in a test, deprecations are not an error. + deprecations = self.useFixture(Deprecations(MODULE)) + deprecations.ignore_deprecations() + warnings.warn('message ignored', DeprecationWarning) + + def test_ignore_deprecations_here(self): + # While in the ignore_deprecations_here() context, deprecations are not + # errors, and afterwards deprecations are errors. + deprecations = self.useFixture(Deprecations(MODULE)) + with deprecations.ignore_deprecations_here(): + warnings.warn('message ignored', DeprecationWarning) + self.assertRaises( + DeprecationWarning, + lambda: warnings.warn('not ignored', DeprecationWarning)) + + def test_other_module(self): + # When the Deprecations fixture is active, deprecations from other + # modules are ignored. + self.useFixture(Deprecations('different_module')) + warnings.warn('message ignored', DeprecationWarning) + + def test_multiple_instances(self): + # When there are multiple Deprecations fixtures in use, one going out + # of scope doesn't mess up the other one. + self.useFixture(Deprecations(MODULE)) + + with Deprecations('different_module'): + self.assertRaises( + DeprecationWarning, + lambda: warnings.warn('not ignored', DeprecationWarning)) + + self.assertRaises( + DeprecationWarning, + lambda: warnings.warn('not ignored', DeprecationWarning))