util.extend_docstring: proper layout for extended class (incl. current return type)

This commit is contained in:
Daniel Otto de Mentock 2022-11-19 08:10:00 +00:00 committed by Martin Diehl
parent dff8a5a7f9
commit 5017aabcea
5 changed files with 276 additions and 49 deletions

View File

@ -94,7 +94,7 @@ class Orientation(Rotation,Crystal):
"""
@util.extend_docstring(_parameter_doc)
@util.extend_docstring(extra_parameters=_parameter_doc)
def __init__(self,
rotation: Union[FloatSequence, Rotation] = np.array([1.,0.,0.,0.]),
*,
@ -300,84 +300,95 @@ class Orientation(Rotation,Crystal):
@classmethod
@util.extended_docstring(Rotation.from_random, _parameter_doc)
@util.extend_docstring(Rotation.from_random,
extra_parameters=_parameter_doc)
def from_random(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_random)
return cls(rotation=Rotation.from_random(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_quaternion,_parameter_doc)
@util.extend_docstring(Rotation.from_quaternion,
extra_parameters=_parameter_doc)
def from_quaternion(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_quaternion)
return cls(rotation=Rotation.from_quaternion(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_Euler_angles,_parameter_doc)
@util.extend_docstring(Rotation.from_Euler_angles,
extra_parameters=_parameter_doc)
def from_Euler_angles(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_Euler_angles)
return cls(rotation=Rotation.from_Euler_angles(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_axis_angle,_parameter_doc)
@util.extend_docstring(Rotation.from_axis_angle,
extra_parameters=_parameter_doc)
def from_axis_angle(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_axis_angle)
return cls(rotation=Rotation.from_axis_angle(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_basis,_parameter_doc)
@util.extend_docstring(Rotation.from_basis,
extra_parameters=_parameter_doc)
def from_basis(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_basis)
return cls(rotation=Rotation.from_basis(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_matrix,_parameter_doc)
@util.extend_docstring(Rotation.from_matrix,
extra_parameters=_parameter_doc)
def from_matrix(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_matrix)
return cls(rotation=Rotation.from_matrix(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_Rodrigues_vector,_parameter_doc)
@util.extend_docstring(Rotation.from_Rodrigues_vector,
extra_parameters=_parameter_doc)
def from_Rodrigues_vector(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_Rodrigues_vector)
return cls(rotation=Rotation.from_Rodrigues_vector(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_homochoric,_parameter_doc)
@util.extend_docstring(Rotation.from_homochoric,
extra_parameters=_parameter_doc)
def from_homochoric(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_homochoric)
return cls(rotation=Rotation.from_homochoric(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_cubochoric,_parameter_doc)
@util.extend_docstring(Rotation.from_cubochoric,
extra_parameters=_parameter_doc)
def from_cubochoric(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_cubochoric)
return cls(rotation=Rotation.from_cubochoric(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_spherical_component,_parameter_doc)
@util.extend_docstring(Rotation.from_spherical_component,
extra_parameters=_parameter_doc)
def from_spherical_component(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_spherical_component)
return cls(rotation=Rotation.from_spherical_component(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extended_docstring(Rotation.from_fiber_component,_parameter_doc)
@util.extend_docstring(Rotation.from_fiber_component,
extra_parameters=_parameter_doc)
def from_fiber_component(cls, **kwargs) -> 'Orientation':
kwargs_rot,kwargs_ori = Orientation._split_kwargs(kwargs,Rotation.from_fiber_component)
return cls(rotation=Rotation.from_fiber_component(**kwargs_rot),**kwargs_ori)
@classmethod
@util.extend_docstring(_parameter_doc)
@util.extend_docstring(extra_parameters=_parameter_doc)
def from_directions(cls,
uvw: FloatSequence,
hkl: FloatSequence,
@ -392,6 +403,10 @@ class Orientation(Rotation,Crystal):
hkl : numpy.ndarray, shape (...,3)
Lattice plane normal aligned with lab frame z-direction.
Returns
-------
new : damask.Orientation
"""
o = cls(**kwargs)
x = o.to_frame(uvw=uvw)
@ -538,8 +553,7 @@ class Orientation(Rotation,Crystal):
Notes
-----
Currently requires same crystal family for both orientations.
For extension to cases with differing symmetry see A. Heinz and P. Neumann 1991 and 10.1107/S0021889808016373.
Requires same crystal family for both orientations.
Examples
--------
@ -569,6 +583,8 @@ class Orientation(Rotation,Crystal):
>>> plt.show()
"""
# For extension to cases with differing symmetry see
# https://doi.org/10.1107/S0021889808016373 and https://doi.org/10.1107/S0108767391006864
if self.family != other.family:
raise NotImplementedError('disorientation between different crystal families')

View File

@ -768,6 +768,10 @@ class Rotation:
P : int {-1,1}, optional
Sign convention. Defaults to -1.
Returns
-------
new : damask.Rotation
"""
qu = np.array(q,dtype=float)
if qu.shape[:-2:-1] != (4,): raise ValueError('invalid shape')
@ -800,6 +804,10 @@ class Rotation:
degrees : bool, optional
Euler angles are given in degrees. Defaults to False.
Returns
-------
new : damask.Rotation
Notes
-----
Bunge Euler angles correspond to a rotation axis sequence of zx'z''.
@ -834,6 +842,10 @@ class Rotation:
P : int {-1,1}, optional
Sign convention. Defaults to -1.
Returns
-------
new : damask.Rotation
"""
ax = np.array(axis_angle,dtype=float)
if ax.shape[:-2:-1] != (4,): raise ValueError('invalid shape')
@ -898,6 +910,10 @@ class Rotation:
R : numpy.ndarray, shape (...,3,3)
Rotation matrix with det(R) = 1 and R.T R = I.
Returns
-------
new : damask.Rotation
"""
return Rotation.from_basis(R)
@ -914,6 +930,10 @@ class Rotation:
b : numpy.ndarray, shape (...,2,3)
Corresponding three-dimensional vectors of second basis.
Returns
-------
new : damask.Rotation
"""
a_ = np.array(a,dtype=float)
b_ = np.array(b,dtype=float)
@ -946,6 +966,10 @@ class Rotation:
P : int {-1,1}, optional
Sign convention. Defaults to -1.
Returns
-------
new : damask.Rotation
"""
ro = np.array(rho,dtype=float)
if ro.shape[:-2:-1] != (4,): raise ValueError('invalid shape')
@ -974,6 +998,10 @@ class Rotation:
P : int {-1,1}, optional
Sign convention. Defaults to -1.
Returns
-------
new : damask.Rotation
"""
ho = np.array(h,dtype=float)
if ho.shape[:-2:-1] != (3,): raise ValueError('invalid shape')
@ -999,6 +1027,10 @@ class Rotation:
P : int {-1,1}, optional
Sign convention. Defaults to -1.
Returns
-------
new : damask.Rotation
"""
cu = np.array(x,dtype=float)
if cu.shape[:-2:-1] != (3,): raise ValueError('invalid shape')
@ -1025,6 +1057,10 @@ class Rotation:
A seed to initialize the BitGenerator.
Defaults to None, i.e. unpredictable entropy will be pulled from the OS.
Returns
-------
new : damask.Rotation
"""
rng = np.random.default_rng(rng_seed)
r = rng.random(3 if shape is None else tuple(shape)+(3,) if hasattr(shape, '__iter__') else (shape,3)) # type: ignore
@ -1066,6 +1102,10 @@ class Rotation:
A seed to initialize the BitGenerator.
Defaults to None, i.e. unpredictable entropy will be pulled from the OS.
Returns
-------
new : damask.Rotation
Notes
-----
Due to the distortion of Euler space in the vicinity of ϕ = 0, probability densities, p, defined on
@ -1150,7 +1190,7 @@ class Rotation:
sigma: float = 0.,
shape: Union[int, IntSequence] = None,
degrees: bool = False,
rng_seed: NumpyRngSeed = None):
rng_seed: NumpyRngSeed = None) -> 'Rotation':
"""
Initialize with samples from a Gaussian distribution around a given direction.
@ -1173,6 +1213,10 @@ class Rotation:
A seed to initialize the BitGenerator.
Defaults to None, i.e. unpredictable entropy will be pulled from the OS.
Returns
-------
new : damask.Rotation
Notes
-----
The crystal direction for (θ=0,φ=0) is [0 0 1],

View File

@ -1,4 +1,3 @@
"""Functionality for generation of seed points for Voronoi or Laguerre tessellation."""
from typing import Tuple as _Tuple

View File

@ -210,6 +210,14 @@ def open_text(fname: _FileHandle,
open(_Path(fname).expanduser(),mode,newline=('\n' if mode == 'w' else None))
def execution_stamp(class_name: str,
function_name: str = None) -> str:
"""Timestamp the execution of a (function within a) class."""
now = _datetime.datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S%z')
_function_name = '' if function_name is None else f'.{function_name}'
return f'damask.{class_name}{_function_name} v{_version} ({now})'
def natural_sort(key: str) -> _List[_Union[int, str]]:
"""
Natural sort.
@ -403,13 +411,6 @@ def project_equal_area(vector: _np.ndarray,
return _np.roll(_np.block([v[...,:2]/_np.sqrt(1.0+_np.abs(v[...,2:3])),_np.zeros_like(v[...,2:3])]),
-shift if keepdims else 0,axis=-1)[...,:3 if keepdims else 2]
def execution_stamp(class_name: str,
function_name: str = None) -> str:
"""Timestamp the execution of a (function within a) class."""
now = _datetime.datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S%z')
_function_name = '' if function_name is None else f'.{function_name}'
return f'damask.{class_name}{_function_name} v{_version} ({now})'
def hybrid_IA(dist: _np.ndarray,
N: int,
@ -528,37 +529,82 @@ def shapeblender(a: _Tuple[int, ...],
return a + b[i:]
def extend_docstring(extra_docstring: str) -> _Callable:
def _docstringer(docstring: _Union[str, _Callable],
extra_parameters: str = None,
# extra_examples: str = None,
# extra_notes: str = None,
return_type: _Union[str, _Callable] = None) -> str:
"""
Decorator: Append to function's docstring.
Extend a docstring.
Parameters
----------
extra_docstring : str
Docstring to append.
docstring : str or callable, optional
Docstring (of callable) to extend.
extra_parameters : str, optional
Additional information to append to Parameters section.
return_type : str or callable, optional
Type of return variable.
"""
def _decorator(func):
func.__doc__ += extra_docstring
return func
return _decorator
docstring_ = str( docstring if isinstance(docstring,str)
else docstring.__doc__ if hasattr(docstring,'__doc__')
else '')
d = dict(Parameters=extra_parameters,
# Examples=extra_examples,
# Notes=extra_notes,
)
for key,extra in [(k,v) for (k,v) in d.items() if v is not None]:
if not (heading := _re.search(fr'^([ ]*){key}\s*\n\1{"-"*len(key)}',
docstring_,flags=_re.MULTILINE)):
raise RuntimeError(f"Docstring {docstring_} lacks a correctly formatted {key} section to insert values into")
content = [line for line in extra.split('\n') if line.strip()]
indent = len(heading.group(1))
shift = min([len(line)-len(line.lstrip(' '))-indent for line in content])
extra = '\n'.join([(line[shift:] if shift > 0 else
f'{" "*-shift}{line}') for line in content])
docstring_ = _re.sub(fr'(^([ ]*){key}\s*\n\2{"-"*len(key)}[\n ]*[A-Za-z0-9 ]*: ([^\n]+\n)*)',
fr'\1{extra}\n',
docstring_,flags=_re.MULTILINE)
if return_type is None:
return docstring_
else:
if isinstance(return_type,str):
return_type_ = return_type
else:
return_class = return_type.__annotations__.get('return','')
return_type_ = (_sys.modules[return_type.__module__].__name__.split('.')[0]
+'.'
+(return_class.__name__ if not isinstance(return_class,str) else return_class)
)
def extended_docstring(f: _Callable,
extra_docstring: str) -> _Callable:
return _re.sub(r'(^([ ]*)Returns\s*\n\2-------\s*\n[ ]*[A-Za-z0-9 ]*: )(.*)\n',
fr'\1{return_type_}\n',
docstring_,flags=_re.MULTILINE)
def extend_docstring(docstring: _Union[str, _Callable] = None,
extra_parameters: str = None) -> _Callable:
"""
Decorator: Combine another function's docstring with a given docstring.
Decorator: Extend the function's docstring.
Parameters
----------
f : function
Function of which the docstring is taken.
extra_docstring : str
Docstring to append.
docstring : str or callable, optional
Docstring to extend. Defaults to that of decorated function.
extra_parameters : str, optional
Additional information to append to Parameters section.
Notes
-----
Return type will become own type if docstring is callable.
"""
def _decorator(func):
func.__doc__ = f.__doc__ + extra_docstring
func.__doc__ = _docstringer(func.__doc__ if docstring is None else docstring,
extra_parameters,
func if isinstance(docstring,_Callable) else None,
)
return func
return _decorator
@ -649,7 +695,6 @@ def Bravais_to_Miller(*,
[0,0,0,1]]))
return _np.einsum('il,...l',basis,axis)
def Miller_to_Bravais(*,
uvw: _np.ndarray = None,
hkl: _np.ndarray = None) -> _np.ndarray:
@ -706,7 +751,6 @@ def dict_prune(d: _Dict) -> _Dict:
return new
def dict_flatten(d: _Dict) -> _Dict:
"""
Recursively remove keys of single-entry dictionaries.

View File

@ -8,7 +8,6 @@ import h5py
from damask import util
class TestUtil:
@pytest.mark.xfail(sys.platform == 'win32', reason='echo is not a Windows command')
@ -208,3 +207,128 @@ class TestUtil:
@pytest.mark.parametrize('kw_Miller,kw_Bravais',[('uvw','uvtw'),('hkl','hkil')])
def test_Bravais_Miller_Bravais(self,vector,kw_Miller,kw_Bravais):
assert np.all(vector == util.Miller_to_Bravais(**{kw_Miller:util.Bravais_to_Miller(**{kw_Bravais:vector})}))
@pytest.mark.parametrize('extra_parameters',["""
p2 : str, optional
p2 description 1
p2 description 2
""",
"""
p2 : str, optional
p2 description 1
p2 description 2
""",
"""
p2 : str, optional
p2 description 1
p2 description 2
"""])
@pytest.mark.parametrize('invalid_docstring',["""
Function description
Parameters ----------
p0 : numpy.ndarray, shape (...,4)
p0 description 1
p0 description 2
p1 : int, optional
p1 description
Remaining description
""",
"""
Function description
Parameters
----------
p0 : numpy.ndarray, shape (...,4)
p0 description 1
p0 description 2
p1 : int, optional
p1 description
Remaining description
""",])
def test_extend_docstring_parameters(self,extra_parameters,invalid_docstring):
test_docstring = """
Function description
Parameters
----------
p0 : numpy.ndarray, shape (...,4)
p0 description 1
p0 description 2
p1 : int, optional
p1 description
Remaining description
"""
invalid_docstring = """
Function description
Parameters ----------
p0 : numpy.ndarray, shape (...,4)
p0 description 1
p0 description 2
p1 : int, optional
p1 description
Remaining description
"""
expected = """
Function description
Parameters
----------
p0 : numpy.ndarray, shape (...,4)
p0 description 1
p0 description 2
p1 : int, optional
p1 description
p2 : str, optional
p2 description 1
p2 description 2
Remaining description
""".split("\n")
assert expected == util._docstringer(test_docstring,extra_parameters).split('\n')
with pytest.raises(RuntimeError):
util._docstringer(invalid_docstring,extra_parameters)
def test_replace_docstring_return_type(self):
class TestClassOriginal:
pass
def original_func() -> TestClassOriginal:
pass
class TestClassDecorated:
def decorated_func_bound(self) -> 'TestClassDecorated':
pass
def decorated_func() -> TestClassDecorated:
pass
original_func.__doc__ = """
Function description/Parameters
Returns
-------
Return value : test_util.TestClassOriginal
Remaining description
"""
expected = """
Function description/Parameters
Returns
-------
Return value : test_util.TestClassDecorated
Remaining description
"""
assert expected == util._docstringer(original_func,return_type=decorated_func)
assert expected == util._docstringer(original_func,return_type=TestClassDecorated.decorated_func_bound)