Skip to content

Commit b527e25

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 b527e25

File tree

2 files changed

+211
-2
lines changed

2 files changed

+211
-2
lines changed

nibabel/deprecated.py

Lines changed: 133 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__]
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,121 @@ 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('* {} a {} as of version: {}'.format(
179+
"Raises" if expired else "Will raise",
180+
expired_class,
181+
until))
182+
message = '\n'.join(messages)
183+
184+
def deprecator(func):
185+
186+
@functools.wraps(func)
187+
def deprecated_func(*args, **kwargs):
188+
if expired:
189+
raise expired_class(message)
190+
warnings.warn(message, warn_class, stacklevel=2)
191+
return func(*args, **kwargs)
192+
193+
deprecated_func.__doc__ = _add_dep_doc(deprecated_func.__doc__,
194+
message)
195+
return deprecated_func
196+
197+
return deprecator

nibabel/tests/test_deprecated.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,24 @@
22
"""
33

44
import warnings
5+
from distutils.version import LooseVersion
56

67
from nose.tools import (assert_true, assert_false, assert_raises,
78
assert_equal, assert_not_equal)
89

9-
from ..deprecated import ModuleProxy, FutureWarningMixin
10+
from .. import deprecated
11+
from ..deprecated import (ModuleProxy, FutureWarningMixin, _ensure_cr,
12+
_add_dep_doc, deprecate_with_version,
13+
ExpiredDeprecationError)
14+
15+
_ORIG_PKG_VERSION = deprecated.PKG_VERSION
16+
17+
def setup():
18+
deprecated.PKG_VERSION = LooseVersion('2.0')
19+
20+
21+
def teardown():
22+
deprecated.PKG_VERSION = _ORIG_PKG_VERSION
1023

1124

1225
def test_module_proxy():
@@ -47,3 +60,67 @@ class E(FutureWarningMixin, C):
4760
warn = warns.pop(0)
4861
assert_equal(warn.category, FutureWarning)
4962
assert_equal(str(warn.message), 'Oh no, not this one')
63+
64+
65+
def test__ensure_cr():
66+
# Make sure text ends with carriage return
67+
assert_equal(_ensure_cr(' foo'), ' foo\n')
68+
assert_equal(_ensure_cr(' foo\n'), ' foo\n')
69+
assert_equal(_ensure_cr(' foo '), ' foo\n')
70+
assert_equal(_ensure_cr('foo '), 'foo\n')
71+
assert_equal(_ensure_cr('foo \n bar'), 'foo \n bar\n')
72+
assert_equal(_ensure_cr('foo \n\n'), 'foo\n')
73+
74+
75+
def test__add_dep_doc():
76+
# Test utility function to add deprecation message to docstring
77+
assert_equal(_add_dep_doc('', 'foo'), 'foo\n')
78+
assert_equal(_add_dep_doc('bar', 'foo'), 'bar\n\nfoo\n')
79+
assert_equal(_add_dep_doc(' bar', 'foo'), ' bar\n\nfoo\n')
80+
assert_equal(_add_dep_doc(' bar', 'foo\n'), ' bar\n\nfoo\n')
81+
assert_equal(_add_dep_doc('bar\n\n', 'foo'), 'bar\n\nfoo\n')
82+
assert_equal(_add_dep_doc('bar\n \n', 'foo'), 'bar\n\nfoo\n')
83+
assert_equal(_add_dep_doc(' bar\n\nSome explanation', 'foo\nbaz'),
84+
' bar\n\nfoo\nbaz\n\nSome explanation\n')
85+
assert_equal(_add_dep_doc(' bar\n\n Some explanation', 'foo\nbaz'),
86+
' bar\n \n foo\n baz\n \n Some explanation\n')
87+
88+
89+
def test_deprecate_with_version():
90+
# Test function deprecation
91+
92+
def func_no_doc(): pass
93+
94+
def func_doc(): "A docstring"
95+
96+
def func_doc_long(): "A docstring\n\n Some text"
97+
98+
func = deprecate_with_version('foo')(func_no_doc)
99+
assert_equal(func.__doc__, 'foo\n')
100+
func = deprecate_with_version('foo')(func_doc)
101+
assert_equal(func.__doc__, 'A docstring\n\nfoo\n')
102+
func = deprecate_with_version('foo')(func_doc_long)
103+
assert_equal(func.__doc__, 'A docstring\n \n foo\n \n Some text\n')
104+
105+
func = deprecate_with_version('foo', '1.1')(func_no_doc)
106+
assert_equal(func.__doc__, 'foo\n\n* deprecated from version: 1.1\n')
107+
func = deprecate_with_version('foo', until='2.4')(func_no_doc)
108+
assert_equal(func.__doc__,
109+
'foo\n\n* Will raise a {} as of version: 2.4\n'
110+
.format(ExpiredDeprecationError))
111+
func = deprecate_with_version('foo', until='1.8')(func_no_doc)
112+
assert_equal(func.__doc__,
113+
'foo\n\n* Raises a {} as of version: 1.8\n'
114+
.format(ExpiredDeprecationError))
115+
func = deprecate_with_version('foo', '1.2', '1.8')(func_no_doc)
116+
assert_equal(func.__doc__,
117+
'foo\n\n* deprecated from version: 1.2\n'
118+
'* Raises a {} as of version: 1.8\n'
119+
.format(ExpiredDeprecationError))
120+
func = deprecate_with_version('foo', '1.2', '1.8')(func_doc_long)
121+
assert_equal(func.__doc__,
122+
'A docstring\n \n foo\n \n'
123+
' * deprecated from version: 1.2\n'
124+
' * Raises a {} as of version: 1.8\n \n'
125+
' Some text\n'
126+
.format(ExpiredDeprecationError))

0 commit comments

Comments
 (0)