"""
Base class for wrapped derived types
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from contextlib import AbstractContextManager
from functools import wraps
from types import TracebackType
from typing import Any, Callable, Optional, TypeVar
import attrs
from attrs import define, field
from typing_extensions import Concatenate, ParamSpec
from fgen_runtime.exceptions import InitialisationError
from fgen_runtime.formatting import to_html, to_pretty, to_str
from fgen_runtime.units import FuncT
INVALID_INSTANCE_INDEX: int = -1
"""
Value used to denote an invalid ``instance_index``.
This can occur value when a wrapper class
has not yet been initialised (connected to a Fortran instance).
"""
[docs]@define
class FinalizableWrapperBase(ABC):
"""
Base class for model wrappers
"""
instance_index: int = field(
validator=attrs.validators.instance_of(int),
default=INVALID_INSTANCE_INDEX,
)
"""
Model index of wrapper Fortran instance
"""
def __str__(self) -> str:
"""
Get string representation of self
"""
return to_str(
self,
self.exposed_attributes,
)
def _repr_pretty_(self, p: Any, cycle: bool) -> None:
"""
Get pretty representation of self
Used by IPython notebooks and other tools
"""
to_pretty(
self,
self.exposed_attributes,
p=p,
cycle=cycle,
)
def _repr_html_(self) -> str:
"""
Get html representation of self
Used by IPython notebooks and other tools
"""
return to_html(
self,
self.exposed_attributes,
)
@property
def initialized(self) -> bool:
"""
Is the instance initialised, i.e. connected to a Fortran instance?
"""
return self.instance_index != INVALID_INSTANCE_INDEX
@property
@abstractmethod
def exposed_attributes(self) -> tuple[str, ...]:
"""
Attributes exposed by this wrapper
"""
...
[docs] @classmethod
@abstractmethod
def from_new_connection(cls) -> FinalizableWrapperBase:
"""
Initialise by establishing a new connection with the Fortran module
This requests a new model index from the Fortran module and then
initialises a class instance
Returns
-------
New class instance
"""
...
[docs] @abstractmethod
def finalize(self) -> None:
"""
Finalise the Fortran instance and set self back to being uninitialised
This method resets ``self.instance_index`` back to
``_UNINITIALISED_instance_index``
Should be decorated with :func:`check_initialised`
"""
# call to Fortran module goes here when implementing
self._uninitialise_instance_index()
def _uninitialise_instance_index(self) -> None:
self.instance_index = INVALID_INSTANCE_INDEX
[docs]@define
class FinalizableWrapperBaseContext(AbstractContextManager): # type: ignore
"""
Context manager for a wrapper
"""
model: FinalizableWrapperBase
"""
model instance to be managed
"""
def __enter__(self) -> FinalizableWrapperBase:
if not self.model.initialized:
raise InitialisationError(self.model)
return self.model
def __exit__(
self,
exc_type: Optional[type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
self.model.finalize()
P = ParamSpec("P")
T = TypeVar("T")
Wrapper = TypeVar("Wrapper", bound=FinalizableWrapperBase)
[docs]def check_initialised(method: FuncT) -> FuncT:
"""
Check that initialisation is called before execution
Parameters
----------
method
Method to wrap
Returns
-------
Wrapped method. Before the method is called, initialisation will first be
checked. If the model wrapper hasn't been initialised before the method is
called, an ``InitialisationError`` will be raised.
"""
@wraps(method)
def checked(
ref: Wrapper,
*args: Any,
**kwargs: Any,
) -> Any:
if not ref.initialized:
raise InitialisationError(ref, method)
return method(ref, *args, **kwargs)
return checked # type: ignore
# Thank you for type hints info
# https://adamj.eu/tech/2021/05/11/python-type-hints-args-and-kwargs/
[docs]def execute_finalize_on_fail(
inst: FinalizableWrapperBase,
func_to_try: Callable[Concatenate[int, P], T],
*args: P.args,
**kwargs: P.kwargs,
) -> T:
"""
Execute a function, finalising the instance before raising if any error occurs
This function is most useful in factory functions where it provides a
clean way of ensuring that any Fortran is freed up in the event of an
initialisation failure for any reason
Parameters
----------
inst
Instance whose model index we will use when executing the functin
func_to_try
Function to try executing, must take ``inst``'s model index as its
first argument
*args
Passed to ``func_to_try``
**kwargs
Passed to ``func_to_try``
Returns
-------
Result of calling ``func_to_try(inst.instance_index, *args, **kwargs)``
Raises
------
Exception
Any exception which occurs when calling ``func_to_try. Before the
exception is raised, ``inst.finalize()`` is called.
"""
try:
return func_to_try(inst.instance_index, *args, **kwargs)
except RuntimeError:
# finalize the instance before raising
inst.finalize()
raise