"""General decorators for the landlab library.
General Landlab decorators
++++++++++++++++++++++++++
.. autosummary::
~use_field_name_or_array
~make_return_array_immutable
~deprecated
"""
import inspect
import os
import textwrap
import warnings
from functools import wraps
import numpy as np
from landlab import FieldError
[docs]
class cache_result_in_object:
[docs]
def __init__(self, cache_as=None):
self._attr = cache_as
def __call__(self, func):
name = self._attr or "_" + func.__name__
@wraps(func)
def _wrapped(obj):
if not hasattr(obj, name):
setattr(obj, name, func(obj))
return getattr(obj, name)
return _wrapped
[docs]
class store_result_in_grid:
[docs]
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))
return getattr(grid, name)
return _wrapped
[docs]
class store_result_in_dataset:
[docs]
def __init__(self, dataset=None, name=None):
self._dataset = dataset
self._attr = name
def __call__(self, func):
@wraps(func)
def _wrapped(grid):
name = self._attr or func.__name__
if self._dataset:
ds = getattr(grid, self._dataset)
else:
ds = grid
if name not in ds:
setattr(grid, self._dataset, ds.update({"name": func(grid)}))
return getattr(grid, self._dataset)[name].values
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 add_signature_to_doc(func):
"""Add a function signature to the first line of a docstring.
Add the signature of a function to the first line of its docstring.
This is useful for functions that are decorated in such a way
that its signature changes.
Parameters
----------
func : function
Function to add signature to.
Returns
-------
str
The new docstring with the function signature on the first line.
Examples
--------
>>> from landlab.utils.decorators import add_signature_to_doc
>>> def foo(arg1, kwd=None):
... '''Do something.'''
... pass
...
>>> print(add_signature_to_doc(foo))
foo(arg1, kwd=None)
<BLANKLINE>
Do something.
"""
return """{name}{argspec}
{body}""".format(
name=func.__name__,
argspec=str(inspect.signature(func)),
body=inspect.getdoc(func),
)
[docs]
class use_field_name_or_array:
"""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), xy_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("elevation", [0, 1, 2, 3, 4, 5], at="cell")
>>> my_func(grid, "elevation")
array([ 0., 2., 4., 6., 8., 10.])
"""
[docs]
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
[docs]
def __call__(self, func):
"""Wrap the function."""
func.__doc__ = add_signature_to_doc(func)
@wraps(func)
def _wrapped(grid, vals, *args, **kwds):
"""Convert the second argument to an array."""
if isinstance(vals, str):
if vals in grid[self._at]:
vals = grid[self._at][vals]
else:
raise FieldError(vals)
else:
vals = np.asarray(vals).reshape(-1)
return func(grid, vals, *args, **kwds)
return _wrapped
[docs]
class use_field_name_array_or_value:
"""Decorate a function so that it accepts a field name, array, or value.
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,
a numpy array, or a value as arguments.
Examples
--------
>>> from landlab import RasterModelGrid
>>> grid = RasterModelGrid((4, 5), xy_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,
the name of a field contained withing the grid, or a value (float, int,
etc.). 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_array_or_value
>>> @use_field_name_array_or_value("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("elevation", [0, 1, 2, 3, 4, 5], at="cell")
>>> my_func(grid, "elevation")
array([ 0., 2., 4., 6., 8., 10.])
The array of values can be a value (float, int, etc.).
>>> my_func(grid, 4.0)
array([8., 8., 8., 8., 8., 8.])
"""
[docs]
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
[docs]
def __call__(self, func):
"""Wrap the function."""
func.__doc__ = add_signature_to_doc(func)
@wraps(func)
def _wrapped(grid, vals, *args, **kwds):
"""Convert the second argument to an array."""
if isinstance(vals, str):
if vals in grid[self._at]:
vals = grid[self._at][vals]
else:
raise FieldError(vals)
else:
expected_size = grid.size(self._at)
vals = np.asarray(vals).ravel()
if vals.size == 1:
vals = np.broadcast_to(vals, (expected_size,))
if vals.size != expected_size:
raise ValueError(
"Array passed to function decorated with "
"use_field_name_array_or_value is not "
"the size of fields at " + self._at
)
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
[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,
stacklevel=2,
)
return func(*args, **kwargs)
_wrapped.__dict__.update(func.__dict__)
return _wrapped
return real_decorator