Source code for fgen.data_models.value
"""
Data model of a value (e.g. parameter, single return value)
"""
from __future__ import annotations
from typing import Any, Optional, TypeVar, Union
from attrs import define, field
from fgen.data_models.unitless_value import UnitlessValue
T = TypeVar("T")
[docs]def no_conversion(inp: T) -> T:
"""
Identity function, just returns what it is given i.e. performs no conversion
The use case for this is disabling type conversion with :mod:`cattrs`
and :mod:`attrs`. In order to get :mod:`cattrs` to not convert stuff,
you have to tell the converter a) to prefer attrs converters (i.e. use
``prefer_attrib_converters=True`` when creating your :mod:`cattrs`
converter) and b) actual supply a converter to your :mod:`attrs`
attribute. If you don't actually want to do any conversion, then you
need an identity function (otherwise, :mod:`cattrs` will assume you
didn't think about conversion and inject its own conversion). This
lets the validator handle type checking etc., rather than having that
check happen in a separate spot (without any context about what is
being checked etc.).
Parameters
----------
inp
Input
Returns
-------
``inp``, unchanged.
"""
return inp
[docs]@define
class Value:
"""
Data model of a value
This defines the value's unit, Fortran data type and other metadata.
It is the combination of a :obj:`UnitlessValueDefinition` and unit information.
"""
definition: UnitlessValue
"""Definition of the value's key information"""
unit: Optional[str] = field(default=None, converter=no_conversion)
"""
Unit of the value
The unit must be able to parsed by `pint`
and be present in the :obj:`pint.UnitRegistry`
being used by the application (normal rules for pint).
Some examples include: "kg", "1 / month".
A unit is required for all numeric-types (i.e. integer, real, complex)
that don't have :attr:`dynamic_unit` set.
For non-numeric values, no unit is unused.
"""
dynamic_unit: Union[bool, str] = False
"""
Whether the unit should be inferred dynamically, rather than statically
If this is ``True``, we will infer the unit
using the units of passed :obj:`pint.Quantity`'s.
When passing these values to Fortran,
the unit will be extracted and passed to Fortran as a string
to the attribute whose
:py:attr:`~fgen.data_models.unitless_value.UnitlessValue.is_fortran_units_holder`
is ``True``.
If :attr:`dynamic_unit` is ``True``, when retrieving the values from Fortran,
the unit will be requested from Fortran too
(from the attribute whose
:py:attr:`~fgen.data_models.unitless_value.UnitlessValue.is_fortran_units_holder`
is ``True``)
and added to the return value to make a :obj:`pint.Quantity` before returning.
If this is a string, we assume this tells us where to retrieve the unit information
from on the Python side (i.e. the string should be valid Python code).
"""
@unit.validator
def _check_unit(self, attribute: Any, value: str | None) -> None:
if self.requires_units:
if value is None:
raise ValueError( # noqa: TRY003
f"A unit is required for: {self.definition.name}"
)
if not isinstance(value, str):
raise TypeError( # noqa: TRY003
f"The unit for {self.definition.name} must be a string, "
f"received: {value}"
)
@property
def requires_units(self) -> bool:
"""
Whether this value requires units or not
Returns
-------
``True`` if this value requires units, ``False`` otherwise.
"""
# TODO: loosen this. Not all numeric types require units
# (e.g. integers that represent sizes of things).
return (not self.dynamic_unit) and self.definition.is_numeric_type