Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Enhancements
Changes

* Remove outdated ImportSpy and ImportManager utilities (#188).
* The ``deprecated`` decorator now issues a DeprecationWarning (using
the Python ``warnings`` module) rather than logging a warning via
the ``logging`` machinery. It no longer tries to remember when
a warning has been previously issued. (#220)

Fixes

Expand Down
35 changes: 35 additions & 0 deletions traits/testing/tests/test_unittest_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,23 @@
#------------------------------------------------------------------------------
import threading
import time
import warnings

from traits import _py2to3

from traits.testing.unittest_tools import unittest
from traits.api import (Bool, Event, Float, HasTraits, Int, List,
on_trait_change)
from traits.testing.api import UnittestTools
from traits.util.api import deprecated


@deprecated("This function is outdated. Use 'shiny' instead!")
def old_and_dull():
""" A deprecated function, for use in assertDeprecated tests.

"""
pass


class TestObject(HasTraits):
Expand Down Expand Up @@ -360,6 +370,31 @@ def condition(a_object):
timeout=10.0,
)

def test_assert_deprecated(self):
with self.assertDeprecated():
old_and_dull()

def test_assert_deprecated_failures(self):
with self.assertRaises(self.failureException):
with self.assertDeprecated():
pass

def test_assert_deprecated_when_warning_already_issued(self):
# Exercise a problematic case where previous calls to a function or
# method that issues a DeprecationWarning have already polluted the
# __warningregistry__. For this, we need a single call-point to
# old_and_dull, since distinct call-points have separate entries in
# __warningregistry__.
def old_and_dull_caller():
old_and_dull()

# Pollute the registry by pre-calling the function.
old_and_dull_caller()

# Check that we can still detect the DeprecationWarning.
with self.assertDeprecated():
old_and_dull_caller()


if __name__ == '__main__':
unittest.main()
26 changes: 25 additions & 1 deletion traits/testing/unittest_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@

import contextlib
import threading
import sys
import warnings

from traits.api import (Any, Event, HasStrictTraits, Instance, Int, List,
Property, Str)
from traits.util.async_trait_wait import wait_for_condition

# Compatibility layer for Python 2.6: try loading unittest2
import sys
from traits import _py2to3
if sys.version_info[:2] == (2, 6):
import unittest2 as unittest
Expand Down Expand Up @@ -439,3 +440,26 @@ def assertEventuallyTrue(self, obj, trait, condition, timeout=5.0):
self.fail(
"Timed out waiting for condition. "
"At timeout, condition was {0}.".format(condition_at_timeout))

@contextlib.contextmanager
def assertDeprecated(self):
"""
Assert that the code inside the with block is deprecated. Intended
for testing uses of traits.util.deprecated.deprecated.

"""
# Ugly hack copied from the core Python code (see
# Lib/test/test_support.py) to reset the warnings registry
# for the module making use of this context manager.
#
# Note that this hack is unnecessary in Python 3.4 and later; see
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we breaking compatibility with Python >= 3.4 by using this hack?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No: it should be entirely harmless on Python >= 3.4: it clears out the __warningregistry__ dict, but on Python 3.4 the __warningregistry__ entries are effectively invalidated (but not removed) every time the warnings filterlist is touched [1], so all we're doing is removing entries from the registry that wouldn't have been used anyway.

The tests pass on Python 3.4, and we have Travis CI running on 3.4 too, so we should be pretty safe.

[1] There's a clever counter mechanism for this; see the Python issue for the details.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add a sys.version check here, and not execute the hack for Python >= 3.4.

Looking closely, it's not quite true to say that it's unnecessary on Python >= 3.4: it's unnecessary on Python >= 3.4.2 (released in October). It's still necessary on 3.4.0 or 3.4.1, so it would be better not to execute the hack for Python >= 3.5. And Python 3.5 doesn't even exist yet.

All in all, I think I'd rather just leave this as it is. :-)

# http://bugs.python.org/issue4180 for the background.
registry = sys._getframe(2).f_globals.get('__warningregistry__')
if registry:
registry.clear()

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always', DeprecationWarning)
yield w
self.assertGreater(len(w), 0, msg="Expected a DeprecationWarning, "
"but none was issued")
37 changes: 15 additions & 22 deletions traits/util/deprecated.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
""" A decorator for marking methods/functions as deprecated. """
#------------------------------------------------------------------------------
# Copyright (c) 2005-2014, Enthought, Inc.
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in /LICENSE.txt and may be redistributed only
# under the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
# Thanks for using Enthought open source!
#------------------------------------------------------------------------------

""" A decorator for marking methods/functions as deprecated. """

# Standard library imports.
import logging

# We only warn about each function or method once!
_cache = {}
import functools
import warnings


def deprecated(message):
Expand All @@ -16,28 +24,13 @@ def deprecated(message):
def decorator(fn):
""" A decorator for marking methods/functions as deprecated. """

@functools.wraps(fn)
def wrapper(*args, **kw):
""" The method/function wrapper. """

global _cache

module_name = fn.__module__
function_name = fn.__name__

if (module_name, function_name) not in _cache:
logging.getLogger(module_name).warning(
'DEPRECATED: %s.%s, %s' % (
module_name, function_name, message
)
)

_cache[(module_name, function_name)] = True

warnings.warn(message, DeprecationWarning, stacklevel=2)
return fn(*args, **kw)

wrapper.__doc__ = fn.__doc__
wrapper.__name__ = fn.__name__

return wrapper

return decorator
Expand Down
59 changes: 59 additions & 0 deletions traits/util/tests/test_deprecated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#------------------------------------------------------------------------------
# Copyright (c) 2005-2014, Enthought, Inc.
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in /LICENSE.txt and may be redistributed only
# under the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
# Thanks for using Enthought open source!
#------------------------------------------------------------------------------

from traits.testing.api import UnittestTools
from traits.testing.unittest_tools import unittest
from traits.util.api import deprecated


@deprecated("Addition is deprecated; use subtraction instead.")
def my_deprecated_addition(x, y):
return x + y


@deprecated("Broken code. Use something else.")
def my_bad_function():
1 / 0


class ClassWithDeprecatedBits(object):
@deprecated('bits are deprecated; use bytes')
def bits(self):
return 42

@deprecated('bytes are deprecated too. Use base 10.')
def bytes(self, required_arg, *args, **kwargs):
return required_arg, args, kwargs


class TestDeprecated(unittest.TestCase, UnittestTools):
def test_deprecated_function(self):
with self.assertDeprecated():
result = my_deprecated_addition(42, 1729)
self.assertEqual(result, 1771)

def test_deprecated_exception_raising_function(self):
with self.assertRaises(ZeroDivisionError):
with self.assertDeprecated():
my_bad_function()

def test_deprecated_method(self):
obj = ClassWithDeprecatedBits()
with self.assertDeprecated():
result = obj.bits()
self.assertEqual(result, 42)

def test_deprecated_method_with_fancy_signature(self):
obj = ClassWithDeprecatedBits()
with self.assertDeprecated():
result = obj.bytes(3, 27, 65, name='Boris', age=-3.2)
self.assertEqual(
result, (3, (27, 65), {'name': 'Boris', 'age': -3.2}))