Skip to content

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

Merged
merged 8 commits into from
Aug 15, 2016
4 changes: 3 additions & 1 deletion nibabel/arraywriters.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ def _check_nan2zero(self, nan2zero):
raise WriterError('Deprecated `nan2zero` argument to `to_fileobj` '
'must be same as class value set in __init__')
warnings.warn('Please remove `nan2zero` from call to ' '`to_fileobj` '
'and use in instance __init__ instead',
'and use in instance __init__ instead.\n'
'* deprecated in version: 2.0\n'
'* will raise error in version: 4.0\n',
DeprecationWarning, stacklevel=3)

def _needs_nan2zero(self):
Expand Down
20 changes: 19 additions & 1 deletion nibabel/deprecated.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
""" Module to help with deprecating classes and modules
""" Module to help with deprecating objects and classes
"""

import warnings

from .deprecator import Deprecator
from .info import cmp_pkg_version


class ModuleProxy(object):
""" Proxy for module that may not yet have been imported
Expand Down Expand Up @@ -63,3 +66,18 @@ def __init__(self, *args, **kwargs):
FutureWarning,
stacklevel=2)
super(FutureWarningMixin, self).__init__(*args, **kwargs)


class VisibleDeprecationWarning(UserWarning):
""" Deprecation warning that will be shown by default

Python >= 2.7 does not show standard DeprecationWarnings by default:

http://docs.python.org/dev/whatsnew/2.7.html#the-future-for-python-2-x

Use this class for cases where we do want to show deprecations by default.
"""
pass


deprecate_with_version = Deprecator(cmp_pkg_version)
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

168 changes: 168 additions & 0 deletions nibabel/deprecator.py
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
5 changes: 5 additions & 0 deletions nibabel/ecat.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from .arraywriters import make_array_writer
from .wrapstruct import WrapStruct
from .fileslice import canonical_slicers, predict_shape, slice2outax
from .deprecated import deprecate_with_version

BLOCK_SIZE = 512

Expand Down Expand Up @@ -846,6 +847,10 @@ def get_subheaders(self):
return self._subheader

@classmethod
@deprecate_with_version('from_filespec class method is deprecated.\n'
'Please use the ``from_file_map`` class method '
'instead.',
'2.1', '4.0')
def from_filespec(klass, filespec):
return klass.from_filename(filespec)

Expand Down
51 changes: 19 additions & 32 deletions nibabel/filebasedimages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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')
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
Loading