Some useful decorators for any situation. Includes runtime type checking.
This packages includes many decorators that will make you write cleaner Python code.
This package requires Python 3.11 or later.
There are multiple options for installing this package.
Run pip install pedantic
.
Run conda install -c conda-forge pedantic
pip install git+https://github.com/LostInDarkMath/pedantic-python-decorators.git@master
pedantic-python-decorators-x.y.z-py-none-any.whl
.pip install pedantic-python-decorators-x.y.z-py3-none-any.whl
.The @pedantic
decorator does the following things:
None
, because None
is only a valid argument, if it is annotated via typing.Optional
.In a nutshell:@pedantic
raises an PedanticException
if one of the following happened:
typing.List
instead of typing.List[int]
.
from pedantic import pedantic
@pedantic
def get_sum_of(values: list[int | float]) -> int:
return sum(values)
get_sum_of(values=[0, 1.2, 3, 5.4]) # this raises the following runtime error:
# Type hint of return value is incorrect: Expected type <class 'int'> but 10.0 of type <class 'float'> was the return value which does not match.
As the name suggests, with @validate
you are able to validate the values that are passed to the decorated function.
That is done in a highly customizable way.
But the highest benefit of this decorator is that it makes it extremely easy to write decoupled easy testable, maintainable and scalable code.
The following example shows the decoupled implementation of a configurable algorithm with the help of @validate
:
import os
from dataclasses import dataclass
from pedantic import validate, ExternalParameter, overrides, Validator, Parameter, Min, ReturnAs
@dataclass(frozen=True)
class Configuration:
iterations: int
max_error: float
class ConfigurationValidator(Validator):
@overrides(Validator)
def validate(self, value: Configuration) -> Configuration:
if value.iterations < 1 or value.max_error < 0:
self.raise_exception(msg=f'Invalid configuration: {value}', value=value)
return value
class ConfigFromEnvVar(ExternalParameter):
""" Reads the configuration from environment variables. """
@overrides(ExternalParameter)
def has_value(self) -> bool:
return 'iterations' in os.environ and 'max_error' in os.environ
@overrides(ExternalParameter)
def load_value(self) -> Configuration:
return Configuration(
iterations=int(os.environ['iterations']),
max_error=float(os.environ['max_error']),
)
class ConfigFromFile(ExternalParameter):
""" Reads the configuration from a config file. """
@overrides(ExternalParameter)
def has_value(self) -> bool:
return os.path.isfile('config.csv')
@overrides(ExternalParameter)
def load_value(self) -> Configuration:
with open(file='config.csv', mode='r') as file:
content = file.readlines()
return Configuration(
iterations=int(content[0].strip('\n')),
max_error=float(content[1]),
)
# choose your configuration source here:
@validate(ConfigFromEnvVar(name='config', validators=[ConfigurationValidator()]), strict=False, return_as=ReturnAs.KWARGS_WITH_NONE)
# @validate(ConfigFromFile(name='config', validators=[ConfigurationValidator()]), strict=False)
# with strict_mode = True (which is the default)
# you need to pass a Parameter for each parameter of the decorated function
# @validate(
# Parameter(name='value', validators=[Min(5, include_boundary=False)]),
# ConfigFromFile(name='config', validators=[ConfigurationValidator()]),
# )
def my_algorithm(value: float, config: Configuration) -> float:
"""
This method calculates something that depends on the given value with considering the configuration.
Note how well this small piece of code is designed:
- Fhe function my_algorithm() need a Configuration but has no knowledge where this come from.
- Furthermore, it doesn't care about parameter validation.
- The ConfigurationValidator doesn't know anything about the creation of the data.
- The @validate decorator is the only you need to change, if you want a different configuration source.
"""
print(value)
print(config)
return value
if __name__ == '__main__':
# we can call the function with a config like there is no decorator.
# This makes testing extremely easy: no config files, no environment variables or stuff like that
print(my_algorithm(value=2, config=Configuration(iterations=3, max_error=4.4)))
os.environ['iterations'] = '12'
os.environ['max_error'] = '3.1415'
# but we also can omit the config and load it implicitly by our custom Parameters
print(my_algorithm(value=42.0))
There are no hard dependencies. But if you want to use some advanced features you need to install the following packages:
@in_subprocess
decoratorFlask
(see unit tests for examples): FlaskParameter
(abstract class)FlaskJsonParameter
FlaskFormParameter
FlaskPathParameter
FlaskGetParameter
FlaskHeaderParameter
GenericFlaskDeserializer
Feel free to contribute by submitting a pull request :)
The usage of decorators may affect the performance of your application.
For this reason, I would highly recommend you to disable the decorators if your code runs in a productive environment.
You can disable pedantic
by set an environment variable:
export ENABLE_PEDANTIC=0
You can also disable or enable the environment variables in your project by calling a method:
from pedantic import enable_pedantic, disable_pedantic
enable_pedantic()
This package is not compatible with compiled source code (e.g. with Nuitka).
That’s because it uses the inspect
module from the standard library which will raise errors like OSError: could not get source code
in case of compiled source code.
Don’t forget to check out the documentation.
Happy coding!