Skip to content

Commit 6a6aa52

Browse files
committed
NF: add routine to deprecate with from/to versions
Add deprecation routine that allows you to specify at which version deprecation began, and at which version deprecation will raise an error.
1 parent e37cb5d commit 6a6aa52

File tree

2 files changed

+271
-2
lines changed

2 files changed

+271
-2
lines changed

nibabel/deprecated.py

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
""" Module to help with deprecating classes and modules
1+
""" Module to help with deprecating objects and classes
22
"""
33

4+
import sys
5+
import re
6+
import functools
47
import warnings
8+
from distutils.version import LooseVersion
9+
10+
11+
def _get_version():
12+
package = sys.modules[__package__.split('.')[0]]
13+
return LooseVersion(getattr(package, '__version__', None))
14+
15+
16+
PKG_VERSION = _get_version()
17+
18+
_LEADING_WHITE = re.compile('^(\s*)')
519

620

721
class ModuleProxy(object):
@@ -63,3 +77,123 @@ def __init__(self, *args, **kwargs):
6377
FutureWarning,
6478
stacklevel=2)
6579
super(FutureWarningMixin, self).__init__(*args, **kwargs)
80+
81+
82+
class VisibleDeprecationWarning(UserWarning):
83+
""" Deprecation warning that will be shown by default
84+
85+
Python >= 2.7 does not show standard DeprecationWarnings by default:
86+
87+
http://docs.python.org/dev/whatsnew/2.7.html#the-future-for-python-2-x
88+
89+
Use this class for cases where we do want to show deprecations by default.
90+
"""
91+
pass
92+
93+
94+
class ExpiredDeprecationError(RuntimeError):
95+
""" Error for expired deprecation
96+
97+
Error raised when a called function or method has passed out of its
98+
deprecation period.
99+
"""
100+
pass
101+
102+
103+
def _ensure_cr(text):
104+
""" Remove trailing whitespace and add carriage return
105+
106+
Ensures that `text` always ends with a carriage return
107+
"""
108+
return text.rstrip() + '\n'
109+
110+
111+
def _add_dep_doc(old_doc, dep_doc):
112+
""" Add deprecation message `dep_doc` to docstring in `old_doc`
113+
114+
Parameters
115+
----------
116+
old_doc : str
117+
Docstring from some object.
118+
dep_doc : str
119+
Deprecation warning to add to top of docstring, after initial line.
120+
121+
Returns
122+
-------
123+
new_doc : str
124+
`old_doc` with `dep_doc` inserted after any first lines of docstring.
125+
"""
126+
if not old_doc:
127+
return _ensure_cr(dep_doc)
128+
old_doc = _ensure_cr(old_doc)
129+
dep_doc = _ensure_cr(dep_doc)
130+
old_lines = old_doc.splitlines()
131+
new_lines = []
132+
for line_no, line in enumerate(old_lines):
133+
if line.strip():
134+
new_lines.append(line)
135+
else:
136+
break
137+
next_line = line_no + 1
138+
if next_line >= len(old_lines):
139+
# nothing following first paragraph, just append message
140+
return old_doc + '\n' + dep_doc
141+
indent = _LEADING_WHITE.match(old_lines[next_line]).group()
142+
dep_lines = [indent + L for L in [''] + dep_doc.splitlines() + ['']]
143+
return '\n'.join(new_lines + dep_lines + old_lines[next_line:]) + '\n'
144+
145+
146+
def deprecate_with_version(message, since='', until='',
147+
warn_class=DeprecationWarning,
148+
expired_class=ExpiredDeprecationError):
149+
""" Decorator that marks function or method as deprecated
150+
151+
The decorated function / method will:
152+
153+
* Raise the given `warning_class` warning when the function / method gets
154+
called, up to (and including) version `until` (if specified);
155+
* Raise the given `expired_class` error when the function / method gets
156+
called, when the package version is greater than version `until` (if
157+
specified).
158+
159+
Parameters
160+
----------
161+
message : str
162+
Message explaining deprecation, giving possible alternatives.
163+
since : str, optional
164+
Released version at which object was first deprecated.
165+
until : str, optional
166+
Last released version at which this function will still raise a
167+
deprecation warning. Versions higher than this will raise an error.
168+
warn_class : class, optional
169+
Class of warning to generate.
170+
"""
171+
messages = [message]
172+
if (since, until) != ('', ''):
173+
messages.append('')
174+
if since:
175+
messages.append('* deprecated from version: ' + since)
176+
if until:
177+
expired = PKG_VERSION > LooseVersion(until)
178+
messages.append('* {} {} as of version: {}'.format(
179+
"Raises" if expired else "Will raise",
180+
expired_class,
181+
until))
182+
else:
183+
expired = False
184+
message = '\n'.join(messages)
185+
186+
def deprecator(func):
187+
188+
@functools.wraps(func)
189+
def deprecated_func(*args, **kwargs):
190+
if expired:
191+
raise expired_class(message)
192+
warnings.warn(message, warn_class, stacklevel=2)
193+
return func(*args, **kwargs)
194+
195+
deprecated_func.__doc__ = _add_dep_doc(deprecated_func.__doc__,
196+
message)
197+
return deprecated_func
198+
199+
return deprecator

nibabel/tests/test_deprecated.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
""" Testing `deprecated` module
22
"""
33

4+
import sys
45
import warnings
6+
from functools import partial
7+
from distutils.version import LooseVersion
58

69
from nose.tools import (assert_true, assert_false, assert_raises,
710
assert_equal, assert_not_equal)
811

9-
from ..deprecated import ModuleProxy, FutureWarningMixin
12+
from .. import deprecated
13+
from ..deprecated import (ModuleProxy, FutureWarningMixin, _ensure_cr,
14+
_add_dep_doc, deprecate_with_version,
15+
ExpiredDeprecationError)
16+
17+
from ..testing import clear_and_catch_warnings
18+
19+
20+
_ORIG_PKG_VERSION = deprecated.PKG_VERSION
21+
_OWN_MODULE = sys.modules[__name__]
22+
23+
24+
def setup():
25+
deprecated.PKG_VERSION = LooseVersion('2.0')
26+
27+
28+
def teardown():
29+
deprecated.PKG_VERSION = _ORIG_PKG_VERSION
1030

1131

1232
def test_module_proxy():
@@ -47,3 +67,118 @@ class E(FutureWarningMixin, C):
4767
warn = warns.pop(0)
4868
assert_equal(warn.category, FutureWarning)
4969
assert_equal(str(warn.message), 'Oh no, not this one')
70+
71+
72+
def test__ensure_cr():
73+
# Make sure text ends with carriage return
74+
assert_equal(_ensure_cr(' foo'), ' foo\n')
75+
assert_equal(_ensure_cr(' foo\n'), ' foo\n')
76+
assert_equal(_ensure_cr(' foo '), ' foo\n')
77+
assert_equal(_ensure_cr('foo '), 'foo\n')
78+
assert_equal(_ensure_cr('foo \n bar'), 'foo \n bar\n')
79+
assert_equal(_ensure_cr('foo \n\n'), 'foo\n')
80+
81+
82+
def test__add_dep_doc():
83+
# Test utility function to add deprecation message to docstring
84+
assert_equal(_add_dep_doc('', 'foo'), 'foo\n')
85+
assert_equal(_add_dep_doc('bar', 'foo'), 'bar\n\nfoo\n')
86+
assert_equal(_add_dep_doc(' bar', 'foo'), ' bar\n\nfoo\n')
87+
assert_equal(_add_dep_doc(' bar', 'foo\n'), ' bar\n\nfoo\n')
88+
assert_equal(_add_dep_doc('bar\n\n', 'foo'), 'bar\n\nfoo\n')
89+
assert_equal(_add_dep_doc('bar\n \n', 'foo'), 'bar\n\nfoo\n')
90+
assert_equal(_add_dep_doc(' bar\n\nSome explanation', 'foo\nbaz'),
91+
' bar\n\nfoo\nbaz\n\nSome explanation\n')
92+
assert_equal(_add_dep_doc(' bar\n\n Some explanation', 'foo\nbaz'),
93+
' bar\n \n foo\n baz\n \n Some explanation\n')
94+
95+
96+
def _make_version(version):
97+
return version if hasattr(version, 'version') else LooseVersion(version)
98+
99+
100+
def assert_deprecated(func, args=(), kwargs=None,
101+
warn_class=DeprecationWarning):
102+
""" Assert that `func` raises deprecation warning """
103+
kwargs = {} if kwargs is None else kwargs
104+
func_module = sys.modules[func.__module__]
105+
with clear_and_catch_warnings(modules=[func_module]) as w:
106+
warnings.simplefilter('always', warn_class)
107+
func(*args, **kwargs)
108+
assert_equal(len(w), 1)
109+
110+
111+
def test_deprecate_with_version():
112+
# Test function deprecation
113+
114+
def func_no_doc(): pass
115+
116+
def func_doc(i): "A docstring"
117+
118+
def func_doc_long(i, j): "A docstring\n\n Some text"
119+
120+
func = deprecate_with_version('foo')(func_no_doc)
121+
with clear_and_catch_warnings(modules=[_OWN_MODULE]) as w:
122+
warnings.simplefilter('always')
123+
assert_equal(func(), None)
124+
assert_equal(len(w), 1)
125+
assert_deprecated(func)
126+
assert_equal(func.__doc__, 'foo\n')
127+
func = deprecate_with_version('foo')(func_doc)
128+
with clear_and_catch_warnings(modules=[_OWN_MODULE]) as w:
129+
warnings.simplefilter('always')
130+
assert_equal(func(1), None)
131+
assert_equal(len(w), 1)
132+
assert_deprecated(func, (1,))
133+
assert_equal(func.__doc__, 'A docstring\n\nfoo\n')
134+
func = deprecate_with_version('foo')(func_doc_long)
135+
with clear_and_catch_warnings(modules=[_OWN_MODULE]) as w:
136+
warnings.simplefilter('always')
137+
assert_equal(func(1, 2), None)
138+
assert_equal(len(w), 1)
139+
assert_deprecated(func, (1, 2))
140+
assert_equal(func.__doc__, 'A docstring\n \n foo\n \n Some text\n')
141+
142+
# Try some since and until versions
143+
func = deprecate_with_version('foo', '1.1')(func_no_doc)
144+
assert_equal(func.__doc__, 'foo\n\n* deprecated from version: 1.1\n')
145+
with clear_and_catch_warnings(modules=[_OWN_MODULE]) as w:
146+
warnings.simplefilter('always')
147+
assert_equal(func(), None)
148+
assert_equal(len(w), 1)
149+
assert_deprecated(func)
150+
func = deprecate_with_version('foo', until='2.4')(func_no_doc)
151+
with clear_and_catch_warnings(modules=[_OWN_MODULE]) as w:
152+
warnings.simplefilter('always')
153+
assert_equal(func(), None)
154+
assert_equal(len(w), 1)
155+
assert_deprecated(func)
156+
assert_equal(func.__doc__,
157+
'foo\n\n* Will raise {} as of version: 2.4\n'
158+
.format(ExpiredDeprecationError))
159+
func = deprecate_with_version('foo', until='1.8')(func_no_doc)
160+
assert_raises(ExpiredDeprecationError, func)
161+
assert_equal(func.__doc__,
162+
'foo\n\n* Raises {} as of version: 1.8\n'
163+
.format(ExpiredDeprecationError))
164+
func = deprecate_with_version('foo', '1.2', '1.8')(func_no_doc)
165+
assert_raises(ExpiredDeprecationError, func)
166+
assert_equal(func.__doc__,
167+
'foo\n\n* deprecated from version: 1.2\n'
168+
'* Raises {} as of version: 1.8\n'
169+
.format(ExpiredDeprecationError))
170+
func = deprecate_with_version('foo', '1.2', '1.8')(func_doc_long)
171+
assert_equal(func.__doc__,
172+
'A docstring\n \n foo\n \n'
173+
' * deprecated from version: 1.2\n'
174+
' * Raises {} as of version: 1.8\n \n'
175+
' Some text\n'
176+
.format(ExpiredDeprecationError))
177+
assert_raises(ExpiredDeprecationError, func)
178+
# Check different warnings and errors
179+
func = deprecate_with_version('foo', warn_class=UserWarning)(func_no_doc)
180+
assert_deprecated(func, warn_class=UserWarning)
181+
func = deprecate_with_version('foo',
182+
expired_class=IOError,
183+
until='1.8')(func_no_doc)
184+
assert_raises(IOError, func)

0 commit comments

Comments
 (0)