Source code for assertionlib.ndrepr

"""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()