# coding: utf-8
# --------------------------------------------------------------------
# Copyright (C) 1991 - 2026 - EDF - www.code-aster.org
# This file is part of code_aster.
#
# code_aster is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# code_aster is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with code_aster. If not, see <http://www.gnu.org/licenses/>.
# --------------------------------------------------------------------
"""
:py:mod:`base_utils` --- General purpose utilities
**************************************************
This modules gives some basic utilities.
"""
import inspect
import sys
from array import array
from collections import UserDict, defaultdict
from decimal import Decimal
from functools import wraps
import numpy
[docs]def no_new_attributes(wrapped_setattr):
"""Raise an error on attempts to add a new attribute, while
allowing existing attributes to be set to new values.
Taken from ?
'Python Cookbook' by Alex Martelli, Anna Ravenscroft, David Ascher,
?6.3. 'Restricting Attribute Setting'
"""
def __setattr__(self, name, value):
if hasattr(self, name): # not a new attribute, allow setting
wrapped_setattr(self, name, value)
else:
raise AttributeError(f"Can't add attribute {name!r} to {self}")
return __setattr__
[docs]def import_object(uri):
"""Load and return a python object (class, function...).
Its `uri` looks like "mainpkg.subpkg.module.object", this means
that "mainpkg.subpkg.module" is imported and "object" is
the object to return.
Arguments:
uri (str): Path to the object to import.
Returns:
object: Imported object.
"""
path = uri.split(".")
modname = ".".join(path[:-1])
if len(modname) == 0:
raise ImportError(f"invalid uri: {uri}")
mod = obj = "?"
objname = path[-1]
try:
__import__(modname)
mod = sys.modules[modname]
except ImportError as err:
raise ImportError(f"can not import module : {modname} ({err})")
try:
obj = getattr(mod, objname)
except AttributeError:
raise AttributeError(
f"object ({objname}) not found in module {modname!r}. "
f"Module content is: {tuple(dir(mod))}"
)
return obj
[docs]def get_caller_context(level):
"""Return the context some levels upper.
Arguments:
level (int): Number of parents in the calling stack. 0 means where
`get_caller_context` is called.
Returns:
dict: 'globals' context at this level.
"""
caller = inspect.currentframe()
for _ in range(level + 1):
caller = caller.f_back
try:
context = caller.f_globals
finally:
del caller
return context
[docs]def force_list(values):
"""Ensure `values` is iterable (list, tuple, array...) and return it as
a list."""
if not is_sequence(values):
values = [values]
return list(values)
[docs]def force_tuple(values):
"""Ensure `values` is iterable (list, tuple, array...) and return it as
a tuple.
"""
return tuple(force_list(values))
[docs]def cmp(a, b, rel_tol=None, abs_tol=None):
"""Compare two values using relative or absolute tolerance
(similar to `math.isclose()`).
Arguments:
a (float): first argument.
b (float): second argument.
rel_tol (float, optional): relative tolerancev, *None* if not used.
abs_tol (float, optional): minimum absolute tolerance, *None* if not used.
Returns:
int: -1 if a < b, 0 if a == b, +1 if a > b using tolerances.
"""
eps = 0.0
if rel_tol is not None:
eps = rel_tol * max(abs(a), abs(b))
if abs_tol is not None:
eps = max(eps, abs_tol)
if abs(a - b) < eps:
return 0
if a - eps < b:
return -1
return 1
[docs]def is_int(obj, onvalue=False):
"""Tell if an object is an integer.
Arguments:
obj (misc): Object to be tested.
onvalue (bool, optional): If *onvalue* is True, accept a float number
that is equal to its integer part. If *False*, acceptance is
only based on the object type.
"""
return isinstance(obj, (int, numpy.integer)) or (onvalue and is_float(obj) and obj == int(obj))
[docs]def is_float(obj):
"""Tell if an object is a float number."""
return isinstance(obj, (float, Decimal, numpy.float32, numpy.float64))
[docs]def is_float_or_int(obj):
"""Tell if an object is a float or an integer."""
return is_float(obj) or is_int(obj)
[docs]def is_complex(obj):
"""Tell if an object is a complex number."""
if (
isinstance(obj, (list, tuple))
and len(obj) == 3
and obj[0] in ("RI", "MP")
and is_float_or_int(obj[1])
and is_float_or_int(obj[2])
):
return True
return isinstance(obj, complex)
[docs]def is_number(obj):
"""Tell if an object is a number."""
return is_float_or_int(obj) or is_complex(obj)
[docs]def is_str(obj):
"""Tell if an object is a string."""
return isinstance(obj, str)
[docs]def is_sequence(obj):
"""Is a sequence (allow iteration, not a string)?"""
return isinstance(obj, (list, tuple, array, numpy.ndarray))
value_is_sequence = is_sequence
[docs]def array_to_list(obj):
"""Convert an object to a list if possible (using `tolist()`) or keep it
unchanged otherwise.
Arguments:
obj (misc): Object to convert.
Returns:
misc: Object unchanged or a list.
"""
try:
return obj.tolist()
except AttributeError:
return obj
[docs]def accept_array(func):
"""Decorator that automatically converts numpy arrays to lists.
Needed to pass an array as argument to a Python/C++ method.
"""
@wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper"""
args = [array_to_list(i) for i in args]
return func(*args, **kwargs)
return wrapper
[docs]class Singleton(type):
"""Singleton implementation in python (Metaclass)."""
# add _singleton_id attribute to the subclasses to be independant of import
# path used
__inst = {}
def __call__(cls, *args, **kws):
cls_id = getattr(cls, "_singleton_id", cls)
if cls_id not in cls.__inst:
cls.__inst[cls_id] = super(Singleton, cls).__call__(*args, **kws)
return cls.__inst[cls_id]
[docs]class ReadOnlyDict(UserDict):
"""Read-only dict object with default value to *None*.
Items can be added but their values can not be changed later.
"""
[docs] def __getitem__(self, key):
"""Disable setitem"""
return self.data.get(key)
[docs] def __setitem__(self, key, value):
"""Disable __setitem__"""
if key in self:
raise AttributeError("ReadOnlyDict: values can not be changed!")
super().__setitem__(key, value)
# aster_pkginfo/aster_config will only be available after installation
try:
from .aster_pkginfo import version_info
except ImportError:
version_info = ()
try:
from .aster_config import config as _cfg
config = ReadOnlyDict(**_cfg)
del _cfg
except ImportError:
config = defaultdict(lambda: None)