# This file is part of the pyMOR project (http://www.pymor.org).
# Copyright 2013-2020 pyMOR developers and contributors. All rights reserved.
# License: BSD 2-Clause License (http://opensource.org/licenses/BSD-2-Clause)
"""This module provides base classes from which most classes in pyMOR inherit.
The purpose of these classes is to provide some common functionality for
all objects in pyMOR. The most notable features provided by :class:`BasicObject`
are the following:
1. :class:`BasicObject` sets class :class:`UberMeta` as metaclass
which itself inherits from :class:`abc.ABCMeta`. Thus it is possible
to define interface classes with abstract methods using the
:func:`abstractmethod` decorator. There are also decorators for
abstract class methods, static methods, and properties.
2. Using metaclass magic, each *class* deriving from :class:`BasicObject`
comes with its own :mod:`~pymor.core.logger` instance accessible through its `logger`
attribute. The logger prefix is automatically set to the class name.
3. Logging can be disabled and re-enabled for each *instance* using the
:meth:`BasicObject.disable_logging` and :meth:`BasicObject.enable_logging`
methods.
4. :meth:`BasicObject.uid` provides a unique id for each instance. While
`id(obj)` is only guaranteed to be unique among all living Python objects,
:meth:`BasicObject.uid` will be (almost) unique among all pyMOR objects
that have ever existed, including previous runs of the application. This
is achieved by building the id from a uuid4 which is newly created for
each pyMOR run and a counter which is increased for any object that requests
an uid.
5. If not set by the user to another value, :attr:`BasicObject.name` is
set to the name of the object's class.
:class:`ImmutableObject` derives from :class:`BasicObject` and adds the following
functionality:
1. Using more metaclass magic, each instance which derives from
:class:`ImmutableObject` is locked after its `__init__` method has returned.
Each attempt to change one of its attributes raises an exception. Private
attributes (of the form `_name`) are exempted from this rule.
2. :meth:`ImmutableObject.with_` can be used to create a copy of an instance with
some changed attributes. E.g. ::
obj.with_(a=x, b=y)
creates a copy with the `a` and `b` attributes of `obj` set to `x` and `y`.
`with_` is implemented by creating a new instance, passing the arguments of
`with_` to `__init__`. The missing `__init__` arguments are taken from instance
attributes of the same name.
"""
import abc
from functools import wraps
import inspect
import itertools
import os
from types import FunctionType
import uuid
from pymor.core import logger
from pymor.core.exceptions import ConstError
from pymor.tools.formatrepr import format_repr, _format_generic
DONT_COPY_DOCSTRINGS = int(os.environ.get('PYMOR_WITH_SPHINX', 0)) == 1
NoneType = type(None)
[docs]class UID:
'''Provides unique, quickly computed ids by combining a session UUID4 with a counter.'''
__slots__ = ['uid']
prefix = f'{uuid.uuid4()}_'
counter = [0]
def __init__(self):
self.uid = self.prefix + str(self.counter[0])
self.counter[0] += 1
def __getstate__(self):
return 1
def __setstate__(self, v):
self.uid = self.prefix + str(self.counter[0])
self.counter[0] += 1
[docs]class BasicObject(metaclass=UberMeta):
"""Base class for most classes in pyMOR.
Attributes
----------
logger
A per-class instance of :class:`logging.Logger` with the class
name as prefix.
logging_disabled
`True` if logging has been disabled.
name
The name of the instance. If not set by the user, the name is
set to the class name.
uid
A unique id for each instance. The uid is obtained by using
:class:`UID` and is unique for all pyMOR objects ever created.
"""
@property
def name(self):
n = getattr(self, '_name', None)
return n or type(self).__name__
@name.setter
def name(self, n):
self._name = n
@property
def logging_disabled(self):
return self._logger is logger.dummy_logger
@property
def logger(self):
return self._logger
[docs] def disable_logging(self, doit=True):
"""Disable logging output for this instance."""
if doit:
self._logger = logger.dummy_logger
else:
del self._logger
[docs] def enable_logging(self, doit=True):
"""Enable logging output for this instance."""
self.disable_logging(not doit)
_uid = None
@property
def uid(self):
if self._uid is None:
self._uid = UID()
return self._uid.uid
def _format_repr(self, max_width, verbosity, override={}):
if verbosity < 3 and self.name == type(self).__name__ and 'name' not in override:
override = dict(override, name=None)
return _format_generic(self, max_width, verbosity, override=override)
[docs] def __repr__(self):
return format_repr(self)
abstractmethod = abc.abstractmethod
abstractproperty = abc.abstractproperty
abstractclassmethod = abc.abstractclassmethod
abstractstaticmethod = abc.abstractstaticmethod
[docs]class classinstancemethod:
def __init__(self, cls_meth):
self.cls_meth = cls_meth
def __get__(self, instance, cls):
if cls is None:
return self
if instance is None:
@wraps(self.cls_meth)
def the_class_method(*args, **kwargs):
return self.cls_meth(cls, *args, **kwargs)
return the_class_method
else:
@wraps(self.inst_meth)
def the_instance_method(*args, **kwargs):
return self.inst_meth(instance, *args, **kwargs)
return the_instance_method
def instancemethod(self, inst_meth):
inst_meth.__doc__ = inst_meth.__doc__ or self.cls_meth.__doc__
self.inst_meth = inst_meth
return self
[docs]class ImmutableObject(BasicObject, metaclass=ImmutableMeta):
"""Base class for immutable objects in pyMOR.
Instances of `ImmutableObject` are immutable in the sense that
after execution of `__init__`, any modification of a non-private
attribute will raise an exception.
.. _ImmutableObjectWarning:
.. warning::
For instances of `ImmutableObject`,
the result of member function calls should be completely
determined by the function's arguments together with the
object's `__init__` arguments and the current state of pyMOR's
global |defaults|.
While, in principle, you are allowed to modify private members after
instance initialization, this should never affect the outcome of
future method calls. In particular, if you update any internal state
after initialization, you have to ensure that this state is not affected
by possible changes of the global :mod:`~pymor.core.defaults`.
"""
_locked = False
# we need to define __init__, otherwise the Python 2 signature hack will fail
def __init__(self):
pass
[docs] def __setattr__(self, key, value):
"""depending on _locked state I delegate the setattr call to object or
raise an Exception
"""
if not self._locked or key[0] == '_':
return object.__setattr__(self, key, value)
else:
raise ConstError(f'Changing "{key}" is not allowed in locked "{self.__class__}"')
[docs] def with_(self, new_type=None, **kwargs):
"""Returns a copy with changed attributes.
A a new class instance is created with the given keyword arguments as
arguments for `__init__`. Missing arguments are obtained form instance
attributes with the
same name.
Parameters
----------
new_type
If not None, return an instance of this class (instead of `type(self)`).
`**kwargs`
Names of attributes to change with their new values. Each attribute name
has to be an argument to `__init__`.
Returns
-------
Copy of `self` with changed attributes.
"""
# fill missing __init__ arguments using instance attributes of same name
for arg in (self._init_arguments if new_type is None else new_type._init_arguments):
if arg not in kwargs:
try:
kwargs[arg] = getattr(self, arg)
except AttributeError:
raise ValueError(f"Cannot find missing __init__ argument '{arg}' for '{self.__class__}' "
f"as attribute of '{self}'")
c = (type(self) if new_type is None else new_type)(**kwargs)
if self.logging_disabled:
c.disable_logging()
return c
def __copy__(self):
return self
def __deepcopy__(self, memo):
return self