Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mne/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
sys_info, _get_extra_data_path, _get_root_dir,
_get_numpy_libs)
from .docs import (copy_function_doc_to_method_doc, copy_doc, linkcode_resolve,
open_docs, deprecated, fill_doc, deprecated_alias,
open_docs, deprecated, fill_doc, deprecated_alias, legacy,
copy_base_doc_to_subclass_doc, docdict as _docdict)
from .fetching import _url_to_local_path
from ._logging import (verbose, logger, set_log_level, set_log_file,
Expand Down
137 changes: 87 additions & 50 deletions mne/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4041,29 +4041,17 @@ def open_docs(kind=None, version=None):
webbrowser.open_new_tab('https://mne.tools/%s/%s' % (version, kind))


# Following deprecated class copied from scikit-learn

# force show of DeprecationWarning even on python 2.7
# make DeprecationWarnings actually show up for users
warnings.filterwarnings('always', category=DeprecationWarning, module='mne')


class deprecated:
"""Mark a function, class, or method as deprecated (decorator).

Originally adapted from sklearn and
http://wiki.python.org/moin/PythonDecoratorLibrary, then modified to make
arguments populate properly following our verbose decorator methods based
on decorator.
class _decorator:
"""Inject code or modify the docstring of a class, method, or function."""

Parameters
----------
extra : str
Extra information beyond just saying the class/function/method
is deprecated.
"""

def __init__(self, extra=''): # noqa: D102
def __init__(self, extra, kind): # noqa: D102
self.kind = kind
self.extra = extra
self.msg = f'{{}} is a {self.kind} {{}}. {self.extra}.'

def __call__(self, obj): # noqa: D105
"""Call.
Expand All @@ -4078,44 +4066,33 @@ def __call__(self, obj): # noqa: D105
obj : object
The modified object.
"""
if isinstance(obj, type):
return self._decorate_class(obj)
if inspect.isclass(obj):
obj_type = 'class'
else:
return self._decorate_fun(obj)

def _decorate_class(self, cls):
msg = f"Class {cls.__name__} is deprecated"
cls.__init__ = self._make_fun(cls.__init__, msg)
return cls

def _decorate_fun(self, fun):
"""Decorate function fun."""
msg = f"Function {fun.__name__} is deprecated"
return self._make_fun(fun, msg)

def _make_fun(self, function, msg):
if self.extra:
msg += "; %s" % self.extra

body = f"""\
def %(name)s(%(signature)s):\n
import warnings
warnings.warn({repr(msg)}, category=DeprecationWarning)
return _function_(%(shortsignature)s)"""
evaldict = dict(_function_=function)
# NB: detecting (bound and unbound) methods seems to be impossible
assert inspect.isfunction(obj), f'decorator used on {type(obj)}'
obj_type = 'function'
msg = self.msg.format(obj.__name__, obj_type)
if obj_type == 'class':
obj.__init__ = self._make_fun(obj.__init__, msg)
return obj
return self._make_fun(obj, msg)

def _make_fun(self, func, body):
evaldict = dict(_function_=func)
fm = FunctionMaker(
function, None, None, None, None, function.__module__)
attrs = dict(__wrapped__=function, __qualname__=function.__qualname__,
__globals__=function.__globals__)
func, None, None, None, None, func.__module__)
attrs = dict(__wrapped__=func, __qualname__=func.__qualname__,
__globals__=func.__globals__)
dep = fm.make(body, evaldict, addsource=True, **attrs)
dep.__doc__ = self._update_doc(dep.__doc__)
dep._deprecated_original = function
dep._deprecated_original = func
return dep

def _update_doc(self, olddoc):
newdoc = ".. warning:: DEPRECATED"
newdoc = f".. warning:: {self.kind.upper()}"
if self.extra:
newdoc = "%s: %s" % (newdoc, self.extra)
newdoc = f'{newdoc}: {self.extra}'
newdoc += '.'
if olddoc:
# Get the spacing right to avoid sphinx warnings
Expand All @@ -4124,11 +4101,40 @@ def _update_doc(self, olddoc):
if li > 0 and len(line.strip()):
n_space = len(line) - len(line.lstrip())
break
newdoc = "%s\n\n%s%s" % (newdoc, ' ' * n_space, olddoc)

newdoc = f"{newdoc}\n\n{' ' * n_space}{olddoc}"
return newdoc


# Following deprecated class copied from scikit-learn
class deprecated(_decorator):
"""Mark a function, class, or method as deprecated (decorator).

Originally adapted from sklearn and
http://wiki.python.org/moin/PythonDecoratorLibrary, then modified to make
arguments populate properly following our verbose decorator methods based
on decorator.

Parameters
----------
extra : str
Extra information beyond just saying the class/function/method is
deprecated. Should be a complete sentence (trailing period will be
added automatically). Will be included in DeprecationWarning messages
and in a sphinx warning box in the docstring.
"""

def __init__(self, extra=''):
super().__init__(extra=extra, kind='deprecated')

def _make_fun(self, func, msg):
body = f"""\
def %(name)s(%(signature)s):\n
import warnings
warnings.warn({repr(msg)}, category=DeprecationWarning)
return _function_(%(shortsignature)s)"""
return super()._make_fun(func=func, body=body)


def deprecated_alias(dep_name, func, removed_in=None):
"""Inject a deprecated alias into the namespace."""
if removed_in is None:
Expand All @@ -4143,6 +4149,37 @@ def deprecated_alias(dep_name, func, removed_in=None):
)(deepcopy(func))


###############################################################################
# "legacy" decorator for parts of our API retained only for backward compat

class legacy(_decorator):
"""Mark a function, class, or method as legacy (decorator).

Parameters
----------
alt : str
Description of the alternate, preferred way to achieve a comparable
result.
extra : str
Extra information beyond just saying the class/function/method is
legacy. Should be a complete sentence (trailing period will be
added automatically). Will be included in logger.info messages
and in a sphinx warning box in the docstring.
"""

def __init__(self, alt, extra=''): # noqa: D102
period = '. ' if len(extra) else ''
extra = f'New code should use {alt}{period}{extra}'
super().__init__(extra=extra, kind='legacy')

def _make_fun(self, func, msg):
body = f"""\
def %(name)s(%(signature)s):\n
from mne.utils import logger
logger.info({repr(msg)})
return _function_(%(shortsignature)s)"""
return super()._make_fun(func=func, body=body)

###############################################################################
# The following tools were adapted (mostly trimmed) from SciPy's doccer.py

Expand Down
65 changes: 48 additions & 17 deletions mne/utils/tests/test_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from mne import open_docs, grade_to_tris
from mne.epochs import add_channels_epochs
from mne.utils import (copy_function_doc_to_method_doc, copy_doc,
linkcode_resolve, deprecated, deprecated_alias)
linkcode_resolve, deprecated, deprecated_alias, legacy,
catch_logging)
import webbrowser


Expand All @@ -28,35 +29,65 @@ def new_func():
assert 'deprecated' not in new_func.__doc__


@deprecated('bad func')
@deprecated('deprecated func')
def deprecated_func():
"""Do something."""
pass


@deprecated('bad class')
class deprecated_class(object):
@legacy('replacement_func')
def legacy_func():
"""Do something."""
pass


@deprecated('deprecated class')
class deprecated_class():

def __init__(self):
pass

@deprecated('bad method')
@deprecated('deprecated method')
def bad(self):
pass


def test_deprecated():
"""Test deprecated function."""
assert 'DEPRECATED' in deprecated_func.__doc__
with pytest.deprecated_call(match='bad func'):
deprecated_func()
assert 'DEPRECATED' in deprecated_class.__init__.__doc__
with pytest.deprecated_call(match='bad class'):
dep = deprecated_class()
assert 'DEPRECATED' in deprecated_class.bad.__doc__
assert 'DEPRECATED' in dep.bad.__doc__
with pytest.deprecated_call(match='bad method'):
dep.bad()
@legacy('replacement_class')
class legacy_class(): # noqa D101

def __init__(self):
pass

@legacy('replacement_method')
def bad(self): # noqa D102
pass


@pytest.mark.parametrize(
('msg', 'klass', 'func'),
(('deprecated', deprecated_class, deprecated_func),
('legacy', legacy_class, legacy_func)))
def test_deprecated_and_legacy(msg, func, klass):
"""Test deprecated and legacy decorators."""
if msg == 'deprecated':
with pytest.deprecated_call(match=f'{msg} class'):
_klass = klass()
with pytest.deprecated_call(match=f'{msg} method'):
_klass.bad()
with pytest.deprecated_call(match=f'{msg} func'):
func()
else:
with catch_logging(verbose='info') as log:
_klass = klass()
_klass.bad()
func()
log = log.getvalue()
for kind in ('class', 'method', 'func'):
assert f'New code should use replacement_{kind}' in log
assert msg.upper() in klass.__init__.__doc__
assert msg.upper() in klass.bad.__doc__
assert msg.upper() in _klass.bad.__doc__
assert msg.upper() in func.__doc__


def test_copy_doc():
Expand Down