"""General decorators for the landlab library.
General Landlab decorators
++++++++++++++++++++++++++
.. autosummary::
    :toctree: generated/
    ~landlab.utils.decorators.use_file_name_or_kwds
    ~landlab.utils.decorators.use_field_name_or_array
    ~landlab.utils.decorators.make_return_array_immutable
    ~landlab.utils.decorators.deprecated
"""
import os
import warnings
from functools import wraps
import textwrap
import numpy as np
import six
from ..core.model_parameter_loader import load_params
[docs]class store_result_in_grid(object):
    def __init__(self, name=None):
        self._attr = name
    def __call__(self, func):
        @wraps(func)
        def _wrapped(grid):
            name = self._attr or '_' + func.__name__
            try:
                getattr(grid, name)
            except AttributeError:
                setattr(grid, name, func(grid))
            finally:
                return getattr(grid, name)
        return _wrapped 
[docs]def read_only_array(func):
    """Decorate a function so that its return array is read-only.
    Parameters
    ----------
    func : function
        A function that returns a numpy array.
    Returns
    -------
    func
        A wrapped function that returns a read-only numpy array.
    """
    @wraps(func)
    def _wrapped(self, *args, **kwds):
        """Make the returned array read-only."""
        array = func(self, *args, **kwds)
        array.flags.writeable = False
        return array
    return _wrapped 
[docs]def use_file_name_or_kwds(func):
    """Decorate a method so that it takes a file name or keywords.
    Parameters
    ----------
    func : A function
        A method function that accepts a ModelGrid as a first argument.
    Returns
    -------
    function
        A function that takes an optional second argument, a file, from which
        to read keywords.
    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> from landlab.utils.decorators import use_file_name_or_kwds
    >>> class MyClass(object):
    ...     @use_file_name_or_kwds
    ...     def __init__(self, grid, kw=0.):
    ...         self.kw = kw
    >>> grid = RasterModelGrid((4, 5))
    >>> foo = MyClass(grid)
    >>> foo.kw
    0.0
    >>> foo = MyClass(grid, "kw: 1945")
    >>> foo.kw
    1945
    >>> foo = MyClass(grid, "kw: 1945", kw=1973)
    >>> foo.kw
    1973
    >>> mpd = \"\"\"
    ... kw: kw value
    ... 1e6
    ... \"\"\"
    >>> foo = MyClass(grid, mpd)
    >>> foo.kw
    1000000.0
    """
    @wraps(func)
    def _wrapped(self, *args, **kwds):
        from ..grid import ModelGrid
        if not isinstance(args[0], ModelGrid):
            raise ValueError('first argument must be a ModelGrid')
        if len(args) == 2:
            warnings.warn(
                "Passing a file to a component's __init__ method is "
                "deprecated. Instead, pass parameters as keywords.",
                category=DeprecationWarning)
            if os.path.isfile(args[1]):
                with open(args[1], 'r') as fp:
                    params = load_params(fp)
            else:
                params = load_params(args[1])
        else:
            params = {}
        params.update(kwds)
        func(self, args[0], **params)
    return _wrapped 
[docs]class use_field_name_or_array(object):
    """Decorate a function so that it accepts a field name or array.
    Parameters
    ----------
    func : function
        A function that accepts a grid and array as arguments.
    at_element : str
        The element type that the field is defined on ('node', 'cell',
        etc.)
    Returns
    -------
    func
        A wrapped function that accepts a grid and either a field name or
        a numpy array.
    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> grid = RasterModelGrid((4, 5), spacing=(1, 2))
    >>> def my_func(grid, vals):
    ...     return grid.area_of_cell * vals
    >>> my_func(grid, np.arange(grid.number_of_cells))
    array([  0.,   2.,   4.,   6.,   8.,  10.])
    Decorate the function so that the second argument can be array-like or
    the name of a field contained withing the grid. The decorator takes a
    single argument that is the name (as a `str`) of the grid element that
    the values are defined on ("node", "cell", etc.).
    >>> from landlab.utils.decorators import use_field_name_or_array
    >>> @use_field_name_or_array('cell')
    ... def my_func(grid, vals):
    ...     return grid.area_of_cell * vals
    The array of values now can be list or anything that can be converted to
    a numpy array.
    >>> my_func(grid, [0, 1, 2, 3, 4, 5])
    array([  0.,   2.,   4.,   6.,   8.,  10.])
    The array of values doesn't have to be flat.
    >>> vals = np.array([[0, 1, 2], [3, 4, 5]])
    >>> my_func(grid, vals)
    array([  0.,   2.,   4.,   6.,   8.,  10.])
    The array of values can be a field name.
    >>> _ = grid.add_field('cell', 'elevation', [0, 1, 2, 3, 4, 5])
    >>> my_func(grid, 'elevation')
    array([  0.,   2.,   4.,   6.,   8.,  10.])
    """
    def __init__(self, at_element):
        """Initialize the decorator.
        Parameters
        ----------
        at_element : str
            The element type that the field is defined on ('node', 'cell',
            etc.)
        """
        self._at = at_element
    def __call__(self, func):
        """Wrap the function."""
        @wraps(func)
        def _wrapped(grid, vals, *args, **kwds):
            """Convert the second argument to an array."""
            if isinstance(vals, six.string_types):
                vals = grid[self._at][vals]
            else:
                vals = np.asarray(vals).flatten()
    
            return func(grid, vals, *args, **kwds)
        return _wrapped 
[docs]def make_return_array_immutable(func):
    """Decorate a function so that its return array is read-only.
    Parameters
    ----------
    func : function
        A function that returns a numpy array.
    Returns
    -------
    func
        A wrapped function that returns a read-only view of an array.
    """
    @wraps(func)
    def _wrapped(self, *args, **kwds):
        """Make the returned array read-only."""
        array = func(self, *args, **kwds)
        immutable_array = array.view()
        immutable_array.flags.writeable = False
        return immutable_array
    return _wrapped 
# def deprecated(use, version):
#     """Mark a function as deprecated.
# 
#     Parameters
#     ----------
#     use : str
#         Name of replacement function to use.
#     version : str
#         Version number when function was marked as deprecated.
# 
#     Returns
#     -------
#     func
#         A wrapped function that issues a deprecation warning.
#     """
#     def real_decorator(func):
# 
#         def _wrapped(*args, **kwargs):
#             """Warn that the function is deprecated before calling it."""
#             warnings.warn(
#                 "Call to deprecated function {name}.".format(
#                     name=func.__name__), category=DeprecationWarning)
#             return func(*args, **kwargs)
#         _wrapped.__dict__.update(func.__dict__)
# 
#         return _wrapped
# 
#     return real_decorator
[docs]def deprecated(use, version):
    """Mark a function as deprecated.
    Parameters
    ----------
    use : str
        Name of replacement function to use.
    version : str
        Version number when function was marked as deprecated.
    Returns
    -------
    func
        A wrapped function that issues a deprecation warning.
    """
    def real_decorator(func):
        warning_str = """
.. note:: This method is deprecated as of Landlab version {ver}.
    Use :func:`{use}` instead.
""".format(ver=version, use=use)
        doc_lines = (func.__doc__ or "").split(os.linesep)
        for lineno, line in enumerate(doc_lines):
            if len(line.rstrip()) == 0:
                break
        head = doc_lines[:lineno]
        body = doc_lines[lineno:]
        head = textwrap.dedent(os.linesep.join(head))
        body = textwrap.dedent(os.linesep.join(body))
        func.__doc__ = os.linesep.join([head, warning_str, body])
        @wraps(func)
        def _wrapped(*args, **kwargs):
            if func.__name__.startswith('_'):
                pass
            else:
                warnings.warn(
                    message="Call to deprecated function {name}.".format(
                        name=func.__name__), category=DeprecationWarning)
            return func(*args, **kwargs)
        _wrapped.__dict__.update(func.__dict__)
        return _wrapped
    return real_decorator