"""A module for holding the :class:`NDRepr` class, a subclass of the builtin :class:`reprlib.Repr` class.
Index
-----
.. currentmodule:: assertionlib.ndrepr
.. autosummary::
NDRepr
Type-specific repr methods:
.. autosummary::
:nosignatures:
NDRepr.repr_float
NDRepr.repr_Exception
NDRepr.repr_Signature
NDRepr.repr_method
NDRepr.repr_method_descriptor
NDRepr.repr_function
NDRepr.repr_builtin_function_or_method
NDRepr.repr_type
NDRepr.repr_module
NDRepr.repr_dict_keys
NDRepr.repr_dict_values
NDRepr.repr_dict_items
NDRepr.repr_Molecule
NDRepr.repr_Settings
NDRepr.repr_Atom
NDRepr.repr_Bond
NDRepr.repr_ndarray
NDRepr.repr_DataFrame
NDRepr.repr_Series
NDRepr.repr_Dataset
API
---
.. autoclass:: NDRepr
.. automethod:: NDRepr.repr_float
.. automethod:: NDRepr.repr_Exception
.. automethod:: NDRepr.repr_Signature
.. automethod:: NDRepr.repr_method
.. automethod:: NDRepr.repr_method_descriptor
.. automethod:: NDRepr.repr_function
.. automethod:: NDRepr.repr_builtin_function_or_method
.. automethod:: NDRepr.repr_type
.. automethod:: NDRepr.repr_module
.. automethod:: NDRepr.repr_dict_keys
.. automethod:: NDRepr.repr_dict_values
.. automethod:: NDRepr.repr_dict_items
.. automethod:: NDRepr.repr_Molecule
.. automethod:: NDRepr.repr_Settings
.. automethod:: NDRepr.repr_Atom
.. automethod:: NDRepr.repr_Bond
.. automethod:: NDRepr.repr_ndarray
.. automethod:: NDRepr.repr_DataFrame
.. automethod:: NDRepr.repr_Series
.. automethod:: NDRepr.repr_Dataset
""" # noqa: E501
import sys
import inspect
import reprlib
import builtins
import textwrap
from itertools import chain, islice
from typing import (
Any,
Callable,
Union,
Tuple,
Optional,
Mapping,
List,
TYPE_CHECKING,
KeysView,
ValuesView,
ItemsView,
)
from nanoutils import raise_if, set_docstring
if TYPE_CHECKING:
import numpy as np
from scm.plams import Molecule, Atom, Bond, Settings
from numpy import ndarray
from pandas import DataFrame, Series
from h5py import Dataset
from types import (BuiltinFunctionType, BuiltinMethodType, ModuleType, FunctionType, MethodType)
if sys.version_info >= (3, 7):
from types import MethodDescriptorType
else:
from typing_extensions import Protocol
class MethodDescriptorType(Protocol):
"""See https://github.com/python/typeshed/blob/master/stdlib/3/types.pyi ."""
__name__: str
__qualname__: str
__objclass__: type
def __call__(self, *args: Any, **kwargs: Any) -> Any: ... # noqa: E302
def __get__(self, obj: Any, type: type = ...) -> Any: ... # noqa: E302
if sys.version_info >= (3, 8):
from typing import TypedDict, Literal
else:
from typing_extensions import TypedDict, Literal
class _FormatDict(TypedDict, total=False):
bool: Callable[[np.bool_], str]
int: Callable[[np.integer[Any]], str]
timedelta: Callable[[np.timedelta64], str]
datetime: Callable[[np.datetime64], str]
float: Callable[[np.floating[Any]], str]
longfloat: Callable[[np.longdouble], str]
complexfloat: Callable[[np.complexfloating[Any, Any]], str]
longcomplexfloat: Callable[[np.clongdouble], str]
void: Callable[[np.void], str]
numpystr: Callable[[Union[str, bytes]], str]
object: Callable[[object], str]
all: Callable[[object], str]
int_kind: Callable[[np.integer[Any]], str]
float_kind: Callable[[np.floating[Any]], str]
complex_kind: Callable[[np.complexfloating[Any, Any]], str]
str_kind: Callable[[Union[str, bytes]], str]
class _FormatOptions(TypedDict, total=False):
precision: int
threshold: int
edgeitems: int
linewidth: int
suppress: bool
nanstr: str
infstr: str
formatter: Optional[_FormatDict]
sign: Literal["-", "+", " "]
floatmode: Literal["fixed", "unique", "maxprec", "maxprec_equal"]
legacy: Literal[False, "1.13"]
else:
Molecule = 'scm.plams.mol.molecule.Molecule'
Atom = 'scm.plams.mol.molecule.Atom'
Bond = 'scm.plams.mol.molecule.Bond'
Settings = 'scm.plams.core.settings.Settings'
ndarray = 'numpy.ndarray'
Series = 'pandas.core.series.Series'
DataFrame = 'pandas.core.frame.DataFrame'
Dataset = 'h5py._h1.dataset.Dataset'
BuiltinFunctionType = BuiltinMethodType = "builtins.builtin_function_or_method"
ModuleType = "builtins.module"
FunctionType = "builtins.function"
MethodType = "builtins.method"
MethodDescriptorType = "builtins.method_descriptor"
_FormatDict = "assertionlib.ndrepr._FormatDict"
_FormatOptions = "assertionlib.ndrepr._FormatOptions"
try:
import numpy as np
NUMPY_EX: Optional[Exception] = None
except Exception as ex:
NUMPY_EX = ex
try:
import pandas as pd
PANDAS_EX: Optional[Exception] = None
except Exception as ex:
PANDAS_EX = ex
__all__ = ['NDRepr', 'aNDRepr']
BuiltinType = Union[BuiltinFunctionType, BuiltinMethodType]
[docs]class NDRepr(reprlib.Repr):
"""A subclass of :class:`reprlib.Repr` with methods for handling additional object types.
Has additional methods for handling:
* PLAMS Molecules, Atoms, Bonds and Settings
* NumPy arrays
* Pandas Series and DataFrames
* Callables
Parameters
----------
**kwargs : object
User-specified values for one or more :class:`NDRepr` instance attributes.
An :exc:`AttributeError` is raised upon encountering unrecognized keys.
Attributes
----------
maxSignature : :class:`int`
The maximum length of callables' signatures before further parameters are truncated.
See also :meth:`NDRepr.repr_Signature`.
maxfloat : :class:`int`
The number of to-be displayed :class:`float` decimals.
See also :meth:`NDRepr.repr_float`.
maxMolecule : :class:`int`
The maximum number of to-be displayed atoms and bonds in PLAMS molecules.
See also :meth:`NDRepr.repr_Molecule`.
maxndarray : :class:`int`
The maximum number of items in a :class:`numpy.ndarray` row.
Passed as argument to the :func:`numpy.printoptions` function:
* :code:`threshold = self.maxndarray`
* :code:`edgeitems = self.maxndarray // 2`
See also :meth:`NDRepr.repr_ndarray`.
maxSeries : :class:`int`
The maximum number of rows per :class:`pandas.Series` instance.
Passed as value to :attr:`pandas.options.display`.
* :code:`pandas.options.display.max_rows = self.series`
See also :meth:`NDRepr.repr_Series`.
maxDataFrame : :class:`int`
The maximum number of rows per :class:`pandas.DataFrame` instance.
Passed as values to :attr:`pandas.options.display`:
* :code:`pandas.options.display.max_rows = self.maxdataframe`
* :code:`pandas.options.display.max_columns = self.maxdataframe // 2`
See also :meth:`NDRepr.repr_DataFrame`.
np_printoptions : :class:`dict`
Additional keyword arguments for :func:`numpy.printoptions`.
.. note::
Arguments provided herein will take priority over those specified internally
in :meth:`NDRepr.repr_ndarray`.
pd_printoptions : :class:`dict`
Additional "keyword arguments" for :attr:`pandas.options`.
.. note::
Arguments provided herein will take priority over those specified internally
in :meth:`NDRepr.repr_DataFrame` and :meth:`NDRepr.repr_Series`.
"""
def __init__(self, **kwargs: Union[int, Mapping[str, Any]]) -> None:
"""Initialize an :class:`NDRepr` instance."""
super().__init__()
self.maxstring: int = 800
# New instance attributes
self.maxSignature: int = self.maxstring - 20
self.maxException: int = 1000
self.maxfloat: int = 4
self.maxndarray: int = 6
self.maxSeries: int = 12
self.maxDataFrame: int = 12
self.maxMolecule: int = 6
self.maxSettings: int = self.maxdict
self.np_printoptions: _FormatOptions = {}
self.pd_printoptions: Mapping[str, Any] = {}
# Update attributes based on **kwargs; raise an error if a key is unrecognized
for k, v in kwargs.items():
if not hasattr(self, k):
raise AttributeError(f'{self.__class__.__name__!r} instance '
f'has no attribute {self.repr(k)}')
setattr(self, k, v)
@set_docstring(reprlib.Repr.repr1.__doc__)
def repr1(self, obj: Any, level: int) -> str:
if isinstance(obj, Exception): # Refer all exceptions NDRepr.repr_Exception()
return self.repr_Exception(obj, level)
return super().repr1(obj, level)
[docs] def repr_float(self, obj: float, level: int) -> str:
"""Create a :class:`str` representation of a :class:`float` instance.""" # noqa
i = self.maxfloat
if 10**i < obj or obj < 10**-i:
return f'{obj:{i}.{i}e}'
return f'{obj:{i}.{i}f}' # Exponential notation
[docs] def repr_Exception(self, obj: Exception, level: int) -> str: # noqa: N802
"""Create a :class:`str` representation of an :exc`Exception` instance."""
value = str(obj)
i = self.maxException
if len(value) > i:
value = value[:i] + '...'
return f'{obj.__class__.__name__}({value})'
# New methods for parsing callables
[docs] def repr_method(self, obj: MethodType, level: int) -> str:
"""Create a :class:`str` representation of a bound method."""
name, signature = self._parse_callable(obj, level)
return f"<bound method '{name}{signature}'>"
[docs] def repr_method_descriptor(self, obj: MethodDescriptorType, level: int) -> str:
"""Create a :class:`str` representation of an unbound method."""
name, signature = self._parse_callable(obj, level)
return f"<method descriptor '{name}{signature}'>"
[docs] def repr_function(self, obj: FunctionType, level: int) -> str:
"""Create a :class:`str` representation of a function."""
name, signature = self._parse_callable(obj, level)
return f"<function '{name}{signature}'>"
[docs] def repr_builtin_function_or_method(self, obj: BuiltinType, level: int) -> str:
"""Create a :class:`str` representation of a builtin function or method."""
name, signature = self._parse_callable(obj, level)
if '.' in obj.__qualname__:
return f"<built-in bound method '{name}{signature}'>"
return f"<built-in function '{name}{signature}'>"
[docs] def repr_type(self, obj: type, level: int) -> str:
"""Create a :class:`str` representation of a :class:`type` object."""
name, signature = self._parse_callable(obj, level)
return f"<class '{name}{signature}'>"
[docs] def repr_module(self, obj: ModuleType, level: int) -> str:
"""Create a :class:`str` representation of a module."""
return f"<module '{obj.__name__}'>"
[docs] def repr_dict_keys(self, obj: KeysView[Any], level: int) -> str:
"""Create a :class:`str` representation of a :class:`~typing.KeysView`."""
name = type(obj).__name__
return f"{name}({self.repr_list(obj, level)})" # type: ignore[arg-type]
[docs] def repr_dict_values(self, obj: ValuesView[Any], level: int) -> str:
"""Create a :class:`str` representation of a :class:`~typing.ValuesView`."""
name = type(obj).__name__
return f"{name}({self.repr_list(obj, level)})" # type: ignore[arg-type]
[docs] def repr_dict_items(self, obj: ItemsView[Any, Any], level: int) -> str:
"""Create a :class:`str` representation of a :class:`~typing.ItemsView`."""
name = type(obj).__name__
return f"{name}({self.repr_list(obj, level)})" # type: ignore[arg-type]
[docs] def repr_Signature(self, obj: inspect.Signature, level: int) -> str: # noqa: N802
"""Create a :class:`str` representation of a :class:`~inspect.Signature` instance."""
i = self.maxSignature
signature = str(obj)
# Return the signature without parameter truncation
if len(signature) <= i:
return signature
# Truncate the number of to-be displayed parameters based the 'level' parameter
param, ret = signature.rsplit(')', 1)
if level <= 0:
return f'(...){ret}'
# Truncate the number of to-be displayed parameters based on self.maxSignature
iterator = iter(param.split(', '))
param_accumulate = next(iterator)
for param in iterator:
signature = f'{param_accumulate}, {param}){ret}'
if len(signature) > i:
param_accumulate += ', ...'
break
param_accumulate += f', {param}'
return f'{param_accumulate}){ret}'
def _parse_callable(self, obj: Callable[..., Any], level: int) -> Tuple[str, str]:
"""Create a :class:`str` representation of the name and signature of a callable."""
# Construct the name of the callable
name: str = getattr(obj, '__qualname__', obj.__name__)
# Construct the signature
try:
_signature = inspect.signature(obj)
signature: str = self.repr1(_signature, level - 1)
except ValueError:
signature = '(...)'
return name, signature
# New PLAMS-related methods
[docs] def repr_Molecule(self, obj: Molecule, level: int) -> str: # noqa: N802
"""Create a :class:`str` representation of a |plams.Molecule| instance."""
if level <= 0:
return f'{obj.__class__.__name__}(...)'
elif not obj:
return f'{obj.__class__.__name__}()'
obj.set_atoms_id()
ret = 'Atoms: \n'
i = self.maxMolecule
# Print atoms
kwargs = {'decimal': self.maxfloat, 'space': 14 - (6 - self.maxfloat)}
for atom in obj.atoms[:i]:
ret += f' {atom.id:<5d}{atom.str(**kwargs).strip()}\n'
if len(obj.atoms) > i:
ret += ' ...\n'
# Print bonds
if len(obj.bonds):
ret += 'Bonds: \n'
for bond in obj.bonds[:i]:
ret += f' ({bond.atom1.id})--{bond.order:1.1f}--({bond.atom2.id})\n'
if len(obj.bonds) > i:
ret += ' ...\n'
# Print lattice vectors
if obj.lattice:
ret += 'Lattice:\n'
for vec in obj.lattice:
ret += ' {:16.10f} {:16.10f} {:16.10f}\n'.format(*vec)
obj.unset_atoms_id()
indent = 4 * ' '
return f'{obj.__class__.__name__}(\n{textwrap.indent(ret[:-1], indent)}\n)'
[docs] def repr_Settings(self, obj: Settings, level: int) -> str: # noqa: N802
"""Create a :class:`str` representation of a |plams.Settings| instance."""
n = len(obj)
if not obj:
return f'{obj.__class__.__name__}()'
elif level <= 0:
return '\n...'
pieces: List[str] = []
indent = 4 * ' '
newlevel = level - 1
for k, v in islice(obj.items(), self.maxSettings):
key = str(k)
value = self.repr1(v, newlevel)
pieces.append(f'\n{key}:')
if type(obj) is type(value):
pieces.append(f'{textwrap.indent(value, indent)}:')
else:
pieces.append(f'{textwrap.indent(value, indent)}')
if n > self.maxSettings:
pieces.append('\n...')
ret = ''.join(pieces)
if level == self.maxlevel:
return f'{obj.__class__.__name__}(\n{textwrap.indent(ret[1:], indent)}\n)'
return ret
[docs] def repr_Atom(self, obj: Atom, level: int) -> str: # noqa: N802
"""Create a :class:`str` representation of a |plams.Atom| instance."""
decimal = self.maxfloat
space = 14 - (6 - decimal) # The default PLAMS values for space and decimal are 14 and 6
ret = obj.str(decimal=decimal, space=space).strip()
return f'{obj.__class__.__name__}({ret})'
[docs] def repr_Bond(self, obj: Bond, level: int) -> str: # noqa: N802
"""Create a :class:`str` representation of a |plams.Bond| instance."""
return f'{obj.__class__.__name__}({obj})'
# NumPy- and Pandas-related methods
[docs] @raise_if(NUMPY_EX)
def repr_ndarray(self, obj: "ndarray[Any, Any]", level: int) -> str:
"""Create a :class:`str` representation of a :class:`numpy.ndarray` instance."""
if level <= 0:
return f'{obj.__class__.__name__}(...)'
kwargs: _FormatOptions = {
'threshold': self.maxndarray,
'edgeitems': self.maxndarray // 2,
'formatter': self._get_ndformatter(obj),
}
kwargs.update(self.np_printoptions)
with np.printoptions(**kwargs):
return builtins.repr(obj)
[docs] @raise_if(PANDAS_EX)
def repr_DataFrame(self, obj: DataFrame, level: int) -> str: # noqa: N802
"""Create a :class:`str` representation of a :class:`pandas.DataFrame` instance."""
if level <= 0:
return f'{obj.__class__.__name__}(...)'
kwargs = {'display.max_rows': self.maxDataFrame,
'display.max_columns': self.maxDataFrame // 2}
kwargs.update(self.pd_printoptions)
args = chain.from_iterable(kwargs.items())
with pd.option_context(*args):
return builtins.repr(obj)
[docs] def repr_Series(self, obj: Series, level: int) -> str: # noqa: N802
"""Create a :class:`str` representation of a :class:`pandas.Series` instance."""
if PANDAS_EX is not None:
raise PANDAS_EX
if level <= 0:
return f'{obj.__class__.__name__}(...)'
kwargs = {'display.max_rows': self.maxSeries}
kwargs.update(self.pd_printoptions)
args = chain.from_iterable(kwargs.items())
with pd.option_context(*args):
return builtins.repr(obj)
[docs] def repr_Dataset(self, obj: Dataset, level: int) -> str: # noqa: N802
"""Create a :class:`str` representation of a :class:`h5py.Dataset` instance."""
return repr(obj)
def _get_ndformatter(self, obj: "ndarray[Any, Any]") -> _FormatDict:
"""Return a value for the **formatter** argument in :func:`numpy.printoptions`."""
if obj.dtype != float and obj.dtype != int:
return {}
try:
max_len = len(str(int(obj.max())))
min_len = len(str(int(obj.min())))
except ValueError: # Raised when encountering zero-sized arrays
width = 0
else:
width = max(max_len, min_len)
if obj.dtype == float:
width += 5
value = '{' + f':{width}.{self.maxfloat}f' + '}'
return {'float': value.format}
else: # obj.dtype == np.dtype(int)
value = '{' + f':{width}d' + '}'
return {'int': value.format}
#: An instance of :class:`NDRepr`.
aNDRepr: NDRepr = NDRepr()