Skip to content

Commit 445fa82

Browse files
committed
NF+TST: add class / function to deprecate with versions
Add deprecation class and function that allows you to specify at which version deprecation began, and at which version deprecation will raise an error.
1 parent 05b0c8e commit 445fa82

File tree

4 files changed

+380
-4
lines changed

4 files changed

+380
-4
lines changed

nibabel/deprecated.py

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

44
import warnings
55

6+
from .deprecator import Deprecator
7+
from .info import cmp_pkg_version
8+
69

710
class ModuleProxy(object):
811
""" Proxy for module that may not yet have been imported
@@ -63,3 +66,18 @@ def __init__(self, *args, **kwargs):
6366
FutureWarning,
6467
stacklevel=2)
6568
super(FutureWarningMixin, self).__init__(*args, **kwargs)
69+
70+
71+
class VisibleDeprecationWarning(UserWarning):
72+
""" Deprecation warning that will be shown by default
73+
74+
Python >= 2.7 does not show standard DeprecationWarnings by default:
75+
76+
http://docs.python.org/dev/whatsnew/2.7.html#the-future-for-python-2-x
77+
78+
Use this class for cases where we do want to show deprecations by default.
79+
"""
80+
pass
81+
82+
83+
deprecate_with_version = Deprecator(cmp_pkg_version)

nibabel/deprecator.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
""" Class for recording and reporting deprecations
2+
"""
3+
4+
import functools
5+
import warnings
6+
import re
7+
8+
_LEADING_WHITE = re.compile('^(\s*)')
9+
10+
11+
class ExpiredDeprecationError(RuntimeError):
12+
""" Error for expired deprecation
13+
14+
Error raised when a called function or method has passed out of its
15+
deprecation period.
16+
"""
17+
pass
18+
19+
20+
def _ensure_cr(text):
21+
""" Remove trailing whitespace and add carriage return
22+
23+
Ensures that `text` always ends with a carriage return
24+
"""
25+
return text.rstrip() + '\n'
26+
27+
28+
def _add_dep_doc(old_doc, dep_doc):
29+
""" Add deprecation message `dep_doc` to docstring in `old_doc`
30+
31+
Parameters
32+
----------
33+
old_doc : str
34+
Docstring from some object.
35+
dep_doc : str
36+
Deprecation warning to add to top of docstring, after initial line.
37+
38+
Returns
39+
-------
40+
new_doc : str
41+
`old_doc` with `dep_doc` inserted after any first lines of docstring.
42+
"""
43+
dep_doc = _ensure_cr(dep_doc)
44+
if not old_doc:
45+
return dep_doc
46+
old_doc = _ensure_cr(old_doc)
47+
old_lines = old_doc.splitlines()
48+
new_lines = []
49+
for line_no, line in enumerate(old_lines):
50+
if line.strip():
51+
new_lines.append(line)
52+
else:
53+
break
54+
next_line = line_no + 1
55+
if next_line >= len(old_lines):
56+
# nothing following first paragraph, just append message
57+
return old_doc + '\n' + dep_doc
58+
indent = _LEADING_WHITE.match(old_lines[next_line]).group()
59+
dep_lines = [indent + L for L in [''] + dep_doc.splitlines() + ['']]
60+
return '\n'.join(new_lines + dep_lines + old_lines[next_line:]) + '\n'
61+
62+
63+
class Deprecator(object):
64+
""" Class to make decorator marking function or method as deprecated
65+
66+
The decorated function / method will:
67+
68+
* Raise the given `warning_class` warning when the function / method gets
69+
called, up to (and including) version `until` (if specified);
70+
* Raise the given `error_class` error when the function / method gets
71+
called, when the package version is greater than version `until` (if
72+
specified).
73+
74+
Parameters
75+
----------
76+
version_comparator : callable
77+
Callable accepting string as argument, and return 1 if string
78+
represents a higher version than encoded in the `version_comparator`, 0
79+
if the version is equal, and -1 if the version is lower. For example,
80+
the `version_comparator` may compare the input version string to the
81+
current package version string.
82+
warn_class : class, optional
83+
Class of warning to generate for deprecation.
84+
error_class : class, optional
85+
Class of error to generate when `version_comparator` returns 1 for a
86+
given argument of ``until`` in the ``__call__`` method (see below).
87+
"""
88+
89+
def __init__(self,
90+
version_comparator,
91+
warn_class=DeprecationWarning,
92+
error_class=ExpiredDeprecationError):
93+
self.version_comparator = version_comparator
94+
self.warn_class = warn_class
95+
self.error_class = error_class
96+
97+
def is_bad_version(self, version_str):
98+
""" Return True if `version_str` is too high
99+
100+
Tests `version_str` with ``self.version_comparator``
101+
102+
Parameters
103+
----------
104+
version_str : str
105+
String giving version to test
106+
107+
Returns
108+
-------
109+
is_bad : bool
110+
True if `version_str` is for version below that expected by
111+
``self.version_comparator``, False otherwise.
112+
"""
113+
return self.version_comparator(version_str) == -1
114+
115+
def __call__(self, message, since='', until='',
116+
warn_class=None, error_class=None):
117+
""" Return decorator function function for deprecation warning / error
118+
119+
Parameters
120+
----------
121+
message : str
122+
Message explaining deprecation, giving possible alternatives.
123+
since : str, optional
124+
Released version at which object was first deprecated.
125+
until : str, optional
126+
Last released version at which this function will still raise a
127+
deprecation warning. Versions higher than this will raise an
128+
error.
129+
warn_class : None or class, optional
130+
Class of warning to generate for deprecation (overrides instance
131+
default).
132+
error_class : None or class, optional
133+
Class of error to generate when `version_comparator` returns 1 for a
134+
given argument of ``until`` (overrides class default).
135+
136+
Returns
137+
-------
138+
deprecator : func
139+
Function returning a decorator.
140+
"""
141+
warn_class = warn_class if warn_class else self.warn_class
142+
error_class = error_class if error_class else self.error_class
143+
messages = [message]
144+
if (since, until) != ('', ''):
145+
messages.append('')
146+
if since:
147+
messages.append('* deprecated from version: ' + since)
148+
if until:
149+
messages.append('* {} {} as of version: {}'.format(
150+
"Raises" if self.is_bad_version(until) else "Will raise",
151+
error_class,
152+
until))
153+
message = '\n'.join(messages)
154+
155+
def deprecator(func):
156+
157+
@functools.wraps(func)
158+
def deprecated_func(*args, **kwargs):
159+
if until and self.is_bad_version(until):
160+
raise error_class(message)
161+
warnings.warn(message, warn_class, stacklevel=2)
162+
return func(*args, **kwargs)
163+
164+
deprecated_func.__doc__ = _add_dep_doc(deprecated_func.__doc__,
165+
message)
166+
return deprecated_func
167+
168+
return deprecator

nibabel/tests/test_deprecated.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,26 @@
33

44
import warnings
55

6-
from nose.tools import (assert_true, assert_false, assert_raises,
7-
assert_equal, assert_not_equal)
6+
from nibabel import info
7+
from nibabel.deprecated import (ModuleProxy, FutureWarningMixin,
8+
deprecate_with_version)
89

9-
from ..deprecated import ModuleProxy, FutureWarningMixin
10+
from nose.tools import (assert_true, assert_equal)
11+
12+
from nibabel.tests.test_deprecator import TestDeprecatorFunc as _TestDF
13+
14+
15+
_PKG_VERSION = info.__version__
16+
17+
18+
def setup():
19+
# Hack nibabel version string
20+
info.cmp_pkg_version.__defaults__ = ('2.0',)
21+
22+
23+
def teardown():
24+
# Hack nibabel version string back again
25+
info.cmp_pkg_version.__defaults__ = (info.__version__,)
1026

1127

1228
def test_module_proxy():
@@ -47,3 +63,9 @@ class E(FutureWarningMixin, C):
4763
warn = warns.pop(0)
4864
assert_equal(warn.category, FutureWarning)
4965
assert_equal(str(warn.message), 'Oh no, not this one')
66+
67+
68+
class TestNibabelDeprecator(_TestDF):
69+
""" Test deprecations against nibabel version """
70+
71+
dep_func = deprecate_with_version

0 commit comments

Comments
 (0)