-
Notifications
You must be signed in to change notification settings - Fork 261
MRG: add routine to deprecate with from/to versions #479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
87ac814
b7448eb
e8654dc
09c63da
1e35fc4
edbdd2c
101af68
f82e217
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
""" Class for recording and reporting deprecations | ||
""" | ||
|
||
import functools | ||
import warnings | ||
import re | ||
|
||
_LEADING_WHITE = re.compile('^(\s*)') | ||
|
||
|
||
class ExpiredDeprecationError(RuntimeError): | ||
""" Error for expired deprecation | ||
|
||
Error raised when a called function or method has passed out of its | ||
deprecation period. | ||
""" | ||
pass | ||
|
||
|
||
def _ensure_cr(text): | ||
""" Remove trailing whitespace and add carriage return | ||
|
||
Ensures that `text` always ends with a carriage return | ||
""" | ||
return text.rstrip() + '\n' | ||
|
||
|
||
def _add_dep_doc(old_doc, dep_doc): | ||
""" Add deprecation message `dep_doc` to docstring in `old_doc` | ||
|
||
Parameters | ||
---------- | ||
old_doc : str | ||
Docstring from some object. | ||
dep_doc : str | ||
Deprecation warning to add to top of docstring, after initial line. | ||
|
||
Returns | ||
------- | ||
new_doc : str | ||
`old_doc` with `dep_doc` inserted after any first lines of docstring. | ||
""" | ||
dep_doc = _ensure_cr(dep_doc) | ||
if not old_doc: | ||
return dep_doc | ||
old_doc = _ensure_cr(old_doc) | ||
old_lines = old_doc.splitlines() | ||
new_lines = [] | ||
for line_no, line in enumerate(old_lines): | ||
if line.strip(): | ||
new_lines.append(line) | ||
else: | ||
break | ||
next_line = line_no + 1 | ||
if next_line >= len(old_lines): | ||
# nothing following first paragraph, just append message | ||
return old_doc + '\n' + dep_doc | ||
indent = _LEADING_WHITE.match(old_lines[next_line]).group() | ||
dep_lines = [indent + L for L in [''] + dep_doc.splitlines() + ['']] | ||
return '\n'.join(new_lines + dep_lines + old_lines[next_line:]) + '\n' | ||
|
||
|
||
class Deprecator(object): | ||
""" Class to make decorator marking function or method as deprecated | ||
|
||
The decorated function / method will: | ||
|
||
* Raise the given `warning_class` warning when the function / method gets | ||
called, up to (and including) version `until` (if specified); | ||
* Raise the given `error_class` error when the function / method gets | ||
called, when the package version is greater than version `until` (if | ||
specified). | ||
|
||
Parameters | ||
---------- | ||
version_comparator : callable | ||
Callable accepting string as argument, and return 1 if string | ||
represents a higher version than encoded in the `version_comparator`, 0 | ||
if the version is equal, and -1 if the version is lower. For example, | ||
the `version_comparator` may compare the input version string to the | ||
current package version string. | ||
warn_class : class, optional | ||
Class of warning to generate for deprecation. | ||
error_class : class, optional | ||
Class of error to generate when `version_comparator` returns 1 for a | ||
given argument of ``until`` in the ``__call__`` method (see below). | ||
""" | ||
|
||
def __init__(self, | ||
version_comparator, | ||
warn_class=DeprecationWarning, | ||
error_class=ExpiredDeprecationError): | ||
self.version_comparator = version_comparator | ||
self.warn_class = warn_class | ||
self.error_class = error_class | ||
|
||
def is_bad_version(self, version_str): | ||
""" Return True if `version_str` is too high | ||
|
||
Tests `version_str` with ``self.version_comparator`` | ||
|
||
Parameters | ||
---------- | ||
version_str : str | ||
String giving version to test | ||
|
||
Returns | ||
------- | ||
is_bad : bool | ||
True if `version_str` is for version below that expected by | ||
``self.version_comparator``, False otherwise. | ||
""" | ||
return self.version_comparator(version_str) == -1 | ||
|
||
def __call__(self, message, since='', until='', | ||
warn_class=None, error_class=None): | ||
""" Return decorator function function for deprecation warning / error | ||
|
||
Parameters | ||
---------- | ||
message : str | ||
Message explaining deprecation, giving possible alternatives. | ||
since : str, optional | ||
Released version at which object was first deprecated. | ||
until : str, optional | ||
Last released version at which this function will still raise a | ||
deprecation warning. Versions higher than this will raise an | ||
error. | ||
warn_class : None or class, optional | ||
Class of warning to generate for deprecation (overrides instance | ||
default). | ||
error_class : None or class, optional | ||
Class of error to generate when `version_comparator` returns 1 for a | ||
given argument of ``until`` (overrides class default). | ||
|
||
Returns | ||
------- | ||
deprecator : func | ||
Function returning a decorator. | ||
""" | ||
warn_class = warn_class if warn_class else self.warn_class | ||
error_class = error_class if error_class else self.error_class | ||
messages = [message] | ||
if (since, until) != ('', ''): | ||
messages.append('') | ||
if since: | ||
messages.append('* deprecated from version: ' + since) | ||
if until: | ||
messages.append('* {} {} as of version: {}'.format( | ||
"Raises" if self.is_bad_version(until) else "Will raise", | ||
error_class, | ||
until)) | ||
message = '\n'.join(messages) | ||
|
||
def deprecator(func): | ||
|
||
@functools.wraps(func) | ||
def deprecated_func(*args, **kwargs): | ||
if until and self.is_bad_version(until): | ||
raise error_class(message) | ||
warnings.warn(message, warn_class, stacklevel=2) | ||
return func(*args, **kwargs) | ||
|
||
deprecated_func.__doc__ = _add_dep_doc(deprecated_func.__doc__, | ||
message) | ||
return deprecated_func | ||
|
||
return deprecator |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,13 +8,12 @@ | |
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
''' Common interface for any image format--volume or surface, binary or xml.''' | ||
|
||
import warnings | ||
|
||
from .externals.six import string_types | ||
from .fileholders import FileHolder | ||
from .filename_parser import (types_filenames, TypesFilenamesError, | ||
splitext_addext) | ||
from .openers import ImageOpener | ||
from .deprecated import deprecate_with_version | ||
|
||
|
||
class ImageFileError(Exception): | ||
|
@@ -212,16 +211,13 @@ def __getitem__(self): | |
''' | ||
raise TypeError("Cannot slice image objects.") | ||
|
||
@deprecate_with_version('get_header method is deprecated.\n' | ||
'Please use the ``img.header`` property ' | ||
'instead.', | ||
'2.1', '4.0') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was at least deprecated in the docs at 2.0.0. Is this meant to indicate the first release a warning was raised? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I was assuming that no-one was reading the docs, and so dated the deprecation from the first warning. |
||
def get_header(self): | ||
""" Get header from image | ||
|
||
Please use the `header` property instead of `get_header`; we will | ||
deprecate this method in future versions of nibabel. | ||
""" | ||
warnings.warn('``get_header`` is deprecated.\n' | ||
'Please use the ``img.header`` property ' | ||
'instead', | ||
DeprecationWarning, stacklevel=2) | ||
return self.header | ||
|
||
def get_filename(self): | ||
|
@@ -268,24 +264,16 @@ def from_filename(klass, filename): | |
file_map = klass.filespec_to_file_map(filename) | ||
return klass.from_file_map(file_map) | ||
|
||
@classmethod | ||
def from_filespec(klass, filespec): | ||
warnings.warn('``from_filespec`` class method is deprecated\n' | ||
'Please use the ``from_filename`` class method ' | ||
'instead', | ||
DeprecationWarning, stacklevel=2) | ||
klass.from_filename(filespec) | ||
|
||
@classmethod | ||
def from_file_map(klass, file_map): | ||
raise NotImplementedError | ||
|
||
@classmethod | ||
@deprecate_with_version('from_files class method is deprecated.\n' | ||
'Please use the ``from_file_map`` class method ' | ||
'instead.', | ||
'1.0', '3.0') | ||
def from_files(klass, file_map): | ||
warnings.warn('``from_files`` class method is deprecated\n' | ||
'Please use the ``from_file_map`` class method ' | ||
'instead', | ||
DeprecationWarning, stacklevel=2) | ||
return klass.from_file_map(file_map) | ||
|
||
@classmethod | ||
|
@@ -326,11 +314,11 @@ def filespec_to_file_map(klass, filespec): | |
return file_map | ||
|
||
@classmethod | ||
@deprecate_with_version('filespec_to_files class method is deprecated.\n' | ||
'Please use the "filespec_to_file_map" class ' | ||
'method instead.', | ||
'1.0', '3.0') | ||
def filespec_to_files(klass, filespec): | ||
warnings.warn('``filespec_to_files`` class method is deprecated\n' | ||
'Please use the ``filespec_to_file_map`` class method ' | ||
'instead', | ||
DeprecationWarning, stacklevel=2) | ||
return klass.filespec_to_file_map(filespec) | ||
|
||
def to_filename(self, filename): | ||
|
@@ -350,20 +338,19 @@ def to_filename(self, filename): | |
self.file_map = self.filespec_to_file_map(filename) | ||
self.to_file_map() | ||
|
||
@deprecate_with_version('to_filespec method is deprecated.\n' | ||
'Please use the "to_filename" method instead.', | ||
'1.0', '3.0') | ||
def to_filespec(self, filename): | ||
warnings.warn('``to_filespec`` is deprecated, please ' | ||
'use ``to_filename`` instead', | ||
DeprecationWarning, stacklevel=2) | ||
self.to_filename(filename) | ||
|
||
def to_file_map(self, file_map=None): | ||
raise NotImplementedError | ||
|
||
@deprecate_with_version('to_files method is deprecated.\n' | ||
'Please use the "to_file_map" method instead.', | ||
'1.0', '3.0') | ||
def to_files(self, file_map=None): | ||
warnings.warn('``to_files`` method is deprecated\n' | ||
'Please use the ``to_file_map`` method ' | ||
'instead', | ||
DeprecationWarning, stacklevel=2) | ||
self.to_file_map(file_map) | ||
|
||
@classmethod | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you want the warning class to be
VisibleDeprecationWarning
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't planning on it. Numpy uses these for deprecations where the user is probably making a coding error. We could promote all our deprecations, but that might be noisy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. Just didn't see where it was being used. I'll have a more thorough review of the whole thing, now I'm awake again.