Type and Value Enforcement#
A core principle of MontePy is that users will make mistakes and sometimes provide invalid values,
either the wrong data type, or a nonsensical value.
Montepy is moving to type annotations, and decorator magic to enforce this validity,
rather than relying on a series of boiler plate type and value checks.
All of the core functionalities required are from montepy.utilities.
Useful types are stored in montepy.types.
Enforcing a Function’s Annotations#
The decorator, args_checked(), will at run time check that all values passed to a
function are of type (and sometimes the correct value), as detailed in the type annotations.
For type enforcement this is simple enough:
>>> from montepy.utilities import *
>>> import montepy.types as ty
>>>
>>> @args_checked
... def foo(a: int) -> int:
... return a
>>> print(foo(1))
1
>>> print(foo("a"))
Traceback (most recent call last):
...
TypeError: Unable to set "a" for "foo" to "a" which is not of type "int"
For more complex types, typing.Union can be used. For instance:
from numbers import Integral
@args_checked
def bar(a: Integral | str):
pass
In this case for both foo and bar the type for a is the exact same.
Note
The pipe syntax is preferred for specifying typing.Union, e.g., Integral | str.
Note
When working with numbers avoid using the types float and int.
An int can substitute for a float in almost all cases, and
sometimes libraries, like numpy, provide their own equivalent types.
Rather you should the numbers package instead.
Specifically numbers.Real and numbers.Integral are the most commonly used types in
MontePy.
Note
args_checked will work recursively through a data structure, so the type:
type FancyData = dict[
tuple[str, Integral],
list[list[Real]]
]
would be properly enforced.
Use with __future__.annotations#
Sometimes it is necessary to include:
from __future__ import annotations
in order avoid circular imports in MontePy. See the python documentation for details.
What this does is treat type annotations as strings, and not evaluate them.
When this is done args_checked will evaluate the type annotation at run time, so import errors will not appear until the function is ran.
Value Enforcement#
MontePy also supports value enforcement in the type annotations,
through typing.Annotated
allowed values can also be specified.
MontePy provides functions for the most common value checks, such as positive(). For instance:
from typing import Annotated
import montepy.types as ty
@args_checked
def foo(a: Annotated[Integral, ty.positive]):
pass
Some enforcers accept arguments, such as greater_than():
@args_checked
def foo(a: Annotated[Integral, ty.greater_than(5)]):
pass
Though you can see how this will become very verbose very quickly.
So montepy.types is meant to store most of these Annotated types.
Note
Multiple arguments can be given Annotated for instance if you needed a value to be in:
(0, 5), you could write:
import montepy.types as ty
def foo(a: Annotated[ty.Real, ty.positive, ty.less_than(5)]):
pass
Writing a Custom Value Enforcer#
MontePy uses higher-order functions, functions that return functions, to create value enforcers. It is helpful to look through the value enforcement process to understand why.
A custom type is created/Annotated. A new type is created with the enforcer stored as
AnnotatedTypeAlias.__metadata__. For some enforcers, such asgreater_than(), this stage will accept arguments.The function is called. At this stage the wrapped function is called, and all of the enforcer functions are called with the actual values passed before the nested decorated function is called. The functions must accept the arguments:
(func_name, name, value).
Some pseudo-implementations may be helpful here.
For a simple implementation, let’s look at how positive() could be written:
def positive(func_name, name, x):
if x <= 0:
raise ValueError(
f"The value, {x}, given to {func_name} for argument, {name} is not positive"
)
For a more complicated scenario let’s look at how you might implement greater_than():
def greater_than(minimum: Real):
def enforcer_generator(func_name, name, x):
if x <= minimum:
raise ValueError(
f"The value, {x}, given to {func_name} for argument, {name} is not greater than the minimum, {minimum}"
)
return enforcer_generator
Example#
Note that this below example is the preferred method to import the types and args_checked.
from montepy.utilities import *
import montepy.types as ty
@args_checked
def foo(a: str, b: ty.PositiveInt):
pass