Skip to content

Commit 20cc0fc

Browse files
Merge pull request #479 from matthew-brett/fancy-deprecations
MRG: 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. Add version comparison routine for our versions, to deal with extra `dev` etc strings at end of version.
2 parents e37cb5d + f82e217 commit 20cc0fc

25 files changed

+786
-143
lines changed

nibabel/arraywriters.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,9 @@ def _check_nan2zero(self, nan2zero):
194194
raise WriterError('Deprecated `nan2zero` argument to `to_fileobj` '
195195
'must be same as class value set in __init__')
196196
warnings.warn('Please remove `nan2zero` from call to ' '`to_fileobj` '
197-
'and use in instance __init__ instead',
197+
'and use in instance __init__ instead.\n'
198+
'* deprecated in version: 2.0\n'
199+
'* will raise error in version: 4.0\n',
198200
DeprecationWarning, stacklevel=3)
199201

200202
def _needs_nan2zero(self):

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/ecat.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from .arraywriters import make_array_writer
5454
from .wrapstruct import WrapStruct
5555
from .fileslice import canonical_slicers, predict_shape, slice2outax
56+
from .deprecated import deprecate_with_version
5657

5758
BLOCK_SIZE = 512
5859

@@ -846,6 +847,10 @@ def get_subheaders(self):
846847
return self._subheader
847848

848849
@classmethod
850+
@deprecate_with_version('from_filespec class method is deprecated.\n'
851+
'Please use the ``from_file_map`` class method '
852+
'instead.',
853+
'2.1', '4.0')
849854
def from_filespec(klass, filespec):
850855
return klass.from_filename(filespec)
851856

nibabel/filebasedimages.py

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@
88
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
99
''' Common interface for any image format--volume or surface, binary or xml.'''
1010

11-
import warnings
12-
1311
from .externals.six import string_types
1412
from .fileholders import FileHolder
1513
from .filename_parser import (types_filenames, TypesFilenamesError,
1614
splitext_addext)
1715
from .openers import ImageOpener
16+
from .deprecated import deprecate_with_version
1817

1918

2019
class ImageFileError(Exception):
@@ -212,16 +211,13 @@ def __getitem__(self):
212211
'''
213212
raise TypeError("Cannot slice image objects.")
214213

214+
@deprecate_with_version('get_header method is deprecated.\n'
215+
'Please use the ``img.header`` property '
216+
'instead.',
217+
'2.1', '4.0')
215218
def get_header(self):
216219
""" Get header from image
217-
218-
Please use the `header` property instead of `get_header`; we will
219-
deprecate this method in future versions of nibabel.
220220
"""
221-
warnings.warn('``get_header`` is deprecated.\n'
222-
'Please use the ``img.header`` property '
223-
'instead',
224-
DeprecationWarning, stacklevel=2)
225221
return self.header
226222

227223
def get_filename(self):
@@ -268,24 +264,16 @@ def from_filename(klass, filename):
268264
file_map = klass.filespec_to_file_map(filename)
269265
return klass.from_file_map(file_map)
270266

271-
@classmethod
272-
def from_filespec(klass, filespec):
273-
warnings.warn('``from_filespec`` class method is deprecated\n'
274-
'Please use the ``from_filename`` class method '
275-
'instead',
276-
DeprecationWarning, stacklevel=2)
277-
klass.from_filename(filespec)
278-
279267
@classmethod
280268
def from_file_map(klass, file_map):
281269
raise NotImplementedError
282270

283271
@classmethod
272+
@deprecate_with_version('from_files class method is deprecated.\n'
273+
'Please use the ``from_file_map`` class method '
274+
'instead.',
275+
'1.0', '3.0')
284276
def from_files(klass, file_map):
285-
warnings.warn('``from_files`` class method is deprecated\n'
286-
'Please use the ``from_file_map`` class method '
287-
'instead',
288-
DeprecationWarning, stacklevel=2)
289277
return klass.from_file_map(file_map)
290278

291279
@classmethod
@@ -326,11 +314,11 @@ def filespec_to_file_map(klass, filespec):
326314
return file_map
327315

328316
@classmethod
317+
@deprecate_with_version('filespec_to_files class method is deprecated.\n'
318+
'Please use the "filespec_to_file_map" class '
319+
'method instead.',
320+
'1.0', '3.0')
329321
def filespec_to_files(klass, filespec):
330-
warnings.warn('``filespec_to_files`` class method is deprecated\n'
331-
'Please use the ``filespec_to_file_map`` class method '
332-
'instead',
333-
DeprecationWarning, stacklevel=2)
334322
return klass.filespec_to_file_map(filespec)
335323

336324
def to_filename(self, filename):
@@ -350,20 +338,19 @@ def to_filename(self, filename):
350338
self.file_map = self.filespec_to_file_map(filename)
351339
self.to_file_map()
352340

341+
@deprecate_with_version('to_filespec method is deprecated.\n'
342+
'Please use the "to_filename" method instead.',
343+
'1.0', '3.0')
353344
def to_filespec(self, filename):
354-
warnings.warn('``to_filespec`` is deprecated, please '
355-
'use ``to_filename`` instead',
356-
DeprecationWarning, stacklevel=2)
357345
self.to_filename(filename)
358346

359347
def to_file_map(self, file_map=None):
360348
raise NotImplementedError
361349

350+
@deprecate_with_version('to_files method is deprecated.\n'
351+
'Please use the "to_file_map" method instead.',
352+
'1.0', '3.0')
362353
def to_files(self, file_map=None):
363-
warnings.warn('``to_files`` method is deprecated\n'
364-
'Please use the ``to_file_map`` method '
365-
'instead',
366-
DeprecationWarning, stacklevel=2)
367354
self.to_file_map(file_map)
368355

369356
@classmethod

0 commit comments

Comments
 (0)