Skip to content

Commit

Permalink
Merge pull request #7611 from nabobalis/dep
Browse files Browse the repository at this point in the history
Updated deprecated to use astropy underneath as we can pass in the wa…
  • Loading branch information
nabobalis committed May 15, 2024
2 parents c41733e + a2fa676 commit e752605
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 258 deletions.
4 changes: 2 additions & 2 deletions sunpy/map/mapbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from matplotlib.backend_bases import FigureCanvasBase
from matplotlib.figure import Figure

from sunpy.util.decorators import active_contexts
from sunpy.util.decorators import ACTIVE_CONTEXTS

try:
from dask.array import Array as DaskArray
Expand Down Expand Up @@ -2720,7 +2720,7 @@ def reproject_to(self, target_wcs, *, algorithm='interpolation', return_footprin
.. minigallery:: sunpy.map.GenericMap.reproject_to
"""
# Check if both context managers are active
if active_contexts.get('propagate_with_solar_surface', False) and active_contexts.get('assume_spherical_screen', False):
if ACTIVE_CONTEXTS.get('propagate_with_solar_surface', False) and ACTIVE_CONTEXTS.get('assume_spherical_screen', False):
warn_user("Using propagate_with_solar_surface and assume_spherical_screen together result in loss of off-disk data.")

try:
Expand Down
300 changes: 72 additions & 228 deletions sunpy/util/decorators.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,37 @@
"""
This module provides SunPy specific decorators.
"""
import types
import inspect
import textwrap
import warnings
import functools
from inspect import Parameter, signature
from functools import wraps
from contextlib import contextmanager

import numpy as np

from sunpy.util.exceptions import SunpyDeprecationWarning, SunpyPendingDeprecationWarning, warn_deprecated
from astropy.utils.decorators import deprecated as _deprecated

__all__ = ['deprecated','sunpycontextmanager', 'active_contexts']
from sunpy.util.exceptions import SunpyDeprecationWarning, warn_deprecated

_NUMPY_COPY_IF_NEEDED = False if np.__version__.startswith("1.") else None
__all__ = ['deprecated', 'sunpycontextmanager', 'ACTIVE_CONTEXTS']

def get_removal_version(since):
# Work out which version this will be removed in
since_major, since_minor = since.split('.')[:2]
since_lts = since_minor == '0'
if since_lts:
major = int(since_major)
minor = int(since_minor) + 1
else:
major = int(since_major) + 1
minor = 1
return major, minor


def deprecated(since, message='', name='', alternative='', pending=False,
obj_type=None):
_NUMPY_COPY_IF_NEEDED = False if np.__version__.startswith("1.") else None
_NOT_FOUND = object()
# Public dictionary for context (de)activation tracking
ACTIVE_CONTEXTS = {}


def deprecated(
since,
message="",
name="",
alternative="",
obj_type=None,
warning_type=SunpyDeprecationWarning
):
"""
Used to mark a function or class as deprecated.
To mark an attribute as deprecated, use deprecated_attribute.
Parameters
----------
since : str
Expand Down Expand Up @@ -63,204 +59,17 @@ def new_function():
An alternative function or class name that the user may use in
place of the deprecated object. The deprecation warning will
tell the user about this alternative if provided.
pending : bool, optional
If True, uses a SunpyPendingDeprecationWarning instead of a
``warning_type``.
obj_type : str, optional
The type of this object, if the automatically determined one
needs to be overridden.
warning_type : Warning
Warning to be issued.
Default is `~.SunpyDeprecationWarning`.
"""
major, minor = get_removal_version(since)
removal_version = f"{major}.{minor}"
# TODO: replace this with the astropy deprecated decorator
return _deprecated(since, message=message, name=name, alternative=alternative, pending=pending,
removal_version=removal_version, obj_type=obj_type,
warning_type=SunpyDeprecationWarning,
pending_warning_type=SunpyPendingDeprecationWarning)


def _deprecated(since, message='', name='', alternative='', pending=False, removal_version=None,
obj_type=None, warning_type=SunpyDeprecationWarning,
pending_warning_type=SunpyPendingDeprecationWarning):
# TODO: remove this once the removal_version kwarg has been added to the upstream
# astropy deprecated decorator
method_types = (classmethod, staticmethod, types.MethodType)

def deprecate_doc(old_doc, message):
"""
Returns a given docstring with a deprecation message prepended
to it.
"""
if not old_doc:
old_doc = ''
old_doc = textwrap.dedent(old_doc).strip('\n')
new_doc = (('\n.. deprecated:: {since}'
'\n {message}\n\n'.format(
**{'since': since, 'message': message.strip()})) + old_doc)
if not old_doc:
# This is to prevent a spurious 'unexpected unindent' warning from
# docutils when the original docstring was blank.
new_doc += r'\ '
return new_doc

def get_function(func):
"""
Given a function or classmethod (or other function wrapper type), get
the function object.
"""
if isinstance(func, method_types):
func = func.__func__
return func

def deprecate_function(func, message, warning_type=warning_type):
"""
Returns a wrapped function that displays ``warning_type``
when it is called.
"""

if isinstance(func, method_types):
func_wrapper = type(func)
else:
def func_wrapper(f):
return f

func = get_function(func)

def deprecated_func(*args, **kwargs):
if pending:
category = pending_warning_type
else:
category = warning_type

warnings.warn(message, category, stacklevel=2)

return func(*args, **kwargs)

# If this is an extension function, we can't call
# functools.wraps on it, but we normally don't care.
# This crazy way to get the type of a wrapper descriptor is
# straight out of the Python 3.3 inspect module docs.
if not isinstance(func, type(str.__dict__['__add__'])):
deprecated_func = functools.wraps(func)(deprecated_func)

deprecated_func.__doc__ = deprecate_doc(
deprecated_func.__doc__, message)

return func_wrapper(deprecated_func)

def deprecate_class(cls, message, warning_type=warning_type):
"""
Update the docstring and wrap the ``__init__`` in-place (or ``__new__``
if the class or any of the bases overrides ``__new__``) so it will give
a deprecation warning when an instance is created.
This won't work for extension classes because these can't be modified
in-place and the alternatives don't work in the general case:
- Using a new class that looks and behaves like the original doesn't
work because the __new__ method of extension types usually makes sure
that it's the same class or a subclass.
- Subclassing the class and return the subclass can lead to problems
with pickle and will look weird in the Sphinx docs.
"""
cls.__doc__ = deprecate_doc(cls.__doc__, message)
if cls.__new__ is object.__new__:
cls.__init__ = deprecate_function(get_function(cls.__init__),
message, warning_type)
else:
cls.__new__ = deprecate_function(get_function(cls.__new__),
message, warning_type)
return cls

def deprecate(obj, message=message, name=name, alternative=alternative,
pending=pending, warning_type=warning_type):
if obj_type is None:
if isinstance(obj, type):
obj_type_name = 'class'
elif inspect.isfunction(obj):
obj_type_name = 'function'
elif inspect.ismethod(obj) or isinstance(obj, method_types):
obj_type_name = 'method'
else:
obj_type_name = 'object'
else:
obj_type_name = obj_type

if not name:
name = get_function(obj).__name__

altmessage = ''
if not message or isinstance(message, type(deprecate)):
if pending:
message = ('The {func} {obj_type} will be deprecated in '
'version {deprecated_version}.')
else:
message = ('The {func} {obj_type} is deprecated and may '
'be removed in {future_version}.')
if alternative:
altmessage = f'\n Use {alternative} instead.'

if removal_version is None:
future_version = 'a future version'
else:
future_version = f'version {removal_version}'

message = ((message.format(**{
'func': name,
'name': name,
'deprecated_version': since,
'future_version': future_version,
'alternative': alternative,
'obj_type': obj_type_name})) +
altmessage)

if isinstance(obj, type):
return deprecate_class(obj, message, warning_type)
else:
return deprecate_function(obj, message, warning_type)

if isinstance(message, type(deprecate)):
return deprecate(message)

return deprecate


class add_common_docstring:
"""
A function decorator that will append and/or prepend an addendum to the
docstring of the target function.
Parameters
----------
append : `str`, optional
A string to append to the end of the functions docstring.
prepend : `str`, optional
A string to prepend to the start of the functions docstring.
**kwargs : `dict`, optional
A dictionary to format append and prepend strings.
"""

def __init__(self, append=None, prepend=None, **kwargs):
if kwargs:
append = append
prepend = prepend
self.append = append
self.prepend = prepend
self.kwargs = kwargs

def __call__(self, func):
func.__doc__ = func.__doc__ if func.__doc__ else ''
self.append = self.append if self.append else ''
self.prepend = self.prepend if self.prepend else ''
if self.append and isinstance(func.__doc__, str):
func.__doc__ += self.append
if self.prepend and isinstance(func.__doc__, str):
func.__doc__ = self.prepend + func.__doc__
if self.kwargs:
func.__doc__ = func.__doc__.format(**self.kwargs)
return func
return _deprecated(
since=since, message=message, name=name, alternative=alternative, pending=False,
obj_type=obj_type, warning_type=warning_type
)


def deprecate_positional_args_since(func=None, *, since):
Expand All @@ -282,13 +91,14 @@ def deprecate_positional_args_since(func=None, *, since):
Notes
-----
Taken from from `scikit-learn <https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/utils/validation.py#L1271>`__.
Taken from from `scikit-learn <https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/utils/validation.py#L40>`__.
Licensed under the BSD, see "licenses/SCIKIT-LEARN.rst".
"""
def _inner_deprecate_positional_args(f):
sig = signature(f)
kwonly_args = []
all_args = []

for name, param in sig.parameters.items():
if param.kind == Parameter.POSITIONAL_OR_KEYWORD:
all_args.append(name)
Expand All @@ -308,11 +118,9 @@ def inner_f(*args, **kwargs):
]
args_msg = ", ".join(args_msg)
warn_deprecated(

f"Pass {args_msg} as keyword args. From version "
f"{since} passing these as positional arguments "
"will result in an error"

f"Pass {args_msg} as keyword args. From version "
f"{since} passing these as positional arguments "
"will result in an error"
)
kwargs.update(zip(sig.parameters, args))
return f(**kwargs)
Expand All @@ -324,8 +132,6 @@ def inner_f(*args, **kwargs):

return _inner_deprecate_positional_args

_NOT_FOUND = object()


def cached_property_based_on(attr_name):
"""
Expand Down Expand Up @@ -410,7 +216,7 @@ def inner(instance, value):
return func(instance, value)
return inner

active_contexts = {} # Public dictionary for context activation tracking


def sunpycontextmanager(func):
"""
Expand All @@ -419,7 +225,7 @@ def sunpycontextmanager(func):
"""
@wraps(func)
def wrapper(*args, **kwargs):
active_contexts[func.__name__] = True
ACTIVE_CONTEXTS[func.__name__] = True
gen = func(*args, **kwargs)
value = next(gen)
try:
Expand All @@ -428,5 +234,43 @@ def wrapper(*args, **kwargs):
gen.throw(e)
else:
next(gen, None)
active_contexts[func.__name__] = False
ACTIVE_CONTEXTS[func.__name__] = False
return contextmanager(wrapper)


class add_common_docstring:
"""
A function decorator that will append and/or prepend an addendum to the
docstring of the target function.
Parameters
----------
append : `str`, optional
A string to append to the end of the functions docstring.
prepend : `str`, optional
A string to prepend to the start of the functions docstring.
**kwargs : `dict`, optional
A dictionary to format append and prepend strings.
"""

def __init__(self, append=None, prepend=None, **kwargs):
if kwargs:
append = append
prepend = prepend
self.append = append
self.prepend = prepend
self.kwargs = kwargs

def __call__(self, func):
func.__doc__ = func.__doc__ or ''
self.append = self.append or ''
self.prepend = self.prepend or ''
if self.append and isinstance(func.__doc__, str):
func.__doc__ += self.append
if self.prepend and isinstance(func.__doc__, str):
func.__doc__ = self.prepend + func.__doc__
if self.kwargs:
func.__doc__ = func.__doc__.format(**self.kwargs)
return func

0 comments on commit e752605

Please sign in to comment.