import copy
from io import StringIO
from collections.abc import Iterable
import abc
from typing import Optional, Union, Any, Type, TypeVar
import numpy as np
import yaml
try:
from yaml import CSafeLoader as SafeLoader
from yaml import CSafeDumper as SafeDumper
except ImportError:
from yaml import SafeLoader # type: ignore[assignment]
from yaml import SafeDumper # type: ignore[assignment]
from ._typehints import FileHandle
from . import Rotation
from . import util
MyType = TypeVar('MyType', bound='YAML')
class NiceDumper(SafeDumper):
"""Improve YAML readability for humans."""
def represent_data(self,
data: Any):
"""Cast YAML objects and their subclasses to dict."""
if isinstance(data, dict) and type(data) != dict:
return self.represent_data(dict(data))
if isinstance(data, np.ndarray):
return self.represent_data(data.tolist())
if isinstance(data, Rotation):
return self.represent_data(data.quaternion.tolist())
if isinstance(data, np.generic):
return self.represent_data(data.item())
return super().represent_data(data)
def ignore_aliases(self,
data: Any) -> bool:
"""Do not use references to existing objects."""
return True
def write_line_break(self,
data: Optional[str] = None): # not for CSafeDumper
"""From https://github.com/yaml/pyyaml/issues/127."""
super().write_line_break(data) # type: ignore[misc]
if len(self.indents) == 1: # type: ignore[attr-defined]
super().write_line_break() # type: ignore[misc]
def increase_indent(self,
flow: bool = False,
indentless: bool = False): # not for CSafeDumper
return super().increase_indent(flow, False) # type: ignore[misc]
[docs]
class YAML(dict):
"""YAML-based configuration."""
def __init__(self,
config: Optional[Union[str, dict[str, Any]]] = None,
**kwargs):
"""
New YAML-based configuration.
Parameters
----------
config : dict or str, optional
YAML. String needs to be valid YAML.
**kwargs : arbitrary key–value pairs, optional
Top-level entries of the configuration.
Notes
-----
Values given as key–value pairs take precedence
over entries with the same key in 'config'.
"""
if isinstance(config,str):
kwargs = yaml.load(config, Loader=SafeLoader) | kwargs
elif isinstance(config,dict):
kwargs = config | kwargs
super().__init__(**kwargs)
def __repr__(self) -> str:
"""
Return repr(self).
Show as in file.
"""
output = StringIO()
self.save(output)
output.seek(0)
return ''.join(output.readlines())
def __copy__(self: MyType) -> MyType:
"""
Return deepcopy(self).
Create deep copy.
"""
return copy.deepcopy(self)
copy = __copy__
def __or__(self: MyType,
other) -> MyType:
"""
Return self|other.
Update configuration with contents of other.
Parameters
----------
other : damask.YAML or dict
Key–value pairs that update self.
Returns
-------
updated : damask.YAML
Updated configuration.
"""
duplicate = self.copy()
duplicate.update(other)
return duplicate
def __ior__(self: MyType,
other) -> MyType:
"""
Return self|=other.
Update configuration with contents of other (in-place).
Parameters
----------
other : damask.YAML or dict
Key–value pairs that update self.
"""
self.update(other)
return self
[docs]
def delete(self: MyType,
keys: Union[Iterable, str]) -> MyType:
"""
Remove configuration keys.
Parameters
----------
keys : iterable or scalar
Label of the key(s) to remove.
Returns
-------
updated : damask.YAML
Updated configuration.
"""
duplicate = self.copy()
for k in util.to_list(keys):
del duplicate[k]
return duplicate
[docs]
@classmethod
def load(cls: Type[MyType],
fname: FileHandle) -> MyType:
"""
Load from YAML file.
Parameters
----------
fname : file, str, or pathlib.Path
Filename or file to read.
Returns
-------
loaded : damask.YAML
YAML from file.
"""
with util.open_text(fname) as fhandle:
return cls(yaml.load(fhandle, Loader=SafeLoader))
[docs]
def save(self,
fname: FileHandle,
**kwargs):
"""
Save to YAML file.
Parameters
----------
fname : file, str, or pathlib.Path
Filename or file to write.
**kwargs : dict
Keyword arguments parsed to yaml.dump.
"""
for key,default in [('width',256),
('default_flow_style',None),
('sort_keys',False),
('Dumper',NiceDumper)]:
if key not in kwargs:
kwargs[key] = default
with util.open_text(fname,'w') as fhandle:
fhandle.write(yaml.dump(self,**kwargs))
@property
@abc.abstractmethod
def is_complete(self):
"""Check for completeness."""
raise NotImplementedError
@property
@abc.abstractmethod
def is_valid(self):
"""Check for valid file layout."""
raise NotImplementedError