Skip to content

Commit

Permalink
datamodel: types: added custom types for float values
Browse files Browse the repository at this point in the history
FloatBase: base type to work with float values
FloatNonNegative: custom type for non-negative float numbers
  • Loading branch information
alesmrazek committed Jan 9, 2025
1 parent 55db265 commit 906af8f
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 2 deletions.
2 changes: 2 additions & 0 deletions python/knot_resolver/datamodel/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
DomainName,
EscapedStr,
EscapedStr32B,
FloatNonNegative,
IDPattern,
Int0_32,
Int0_512,
Expand Down Expand Up @@ -37,6 +38,7 @@
"DomainName",
"EscapedStr",
"EscapedStr32B",
"FloatNonNegative",
"IDPattern",
"Int0_32",
"Int0_512",
Expand Down
71 changes: 71 additions & 0 deletions python/knot_resolver/datamodel/types/base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,48 @@ def json_schema(cls: Type["IntBase"]) -> Dict[Any, Any]:
return {"type": "integer"}


class FloatBase(BaseValueType):
"""
Base class to work with float value.
"""

_orig_value: float
_value: float

def __init__(self, source_value: Any, object_path: str = "/") -> None:
if isinstance(source_value, float) and not isinstance(source_value, bool):
self._orig_value = source_value
self._value = source_value
else:
raise ValueError(
f"Unexpected value for '{type(self)}'."
f" Expected float, got '{source_value}' with type '{type(source_value)}'",
object_path,
)

def __int__(self) -> int:
return int(self._value)

def __float__(self) -> float:
return self._value

def __str__(self) -> str:
return str(self._value)

def __repr__(self) -> str:
return f'{type(self).__name__}("{self._value}")'

def __eq__(self, o: object) -> bool:
return isinstance(o, FloatBase) and o._value == self._value

def serialize(self) -> Any:
return self._orig_value

@classmethod
def json_schema(cls: Type["FloatBase"]) -> Dict[Any, Any]:
return {"type": "number"}


class StrBase(BaseValueType):
"""
Base class to work with string value.
Expand Down Expand Up @@ -151,6 +193,35 @@ def json_schema(cls: Type["IntRangeBase"]) -> Dict[Any, Any]:
return typ


class FloatRangeBase(FloatBase):
"""
Base class to work with float value in range.
Just inherit the class and set the values for '_min' and '_max'.
class FloatNonNegative(IntRangeBase):
_min: float = 0.0
"""

_min: float
_max: float

def __init__(self, source_value: Any, object_path: str = "/") -> None:
super().__init__(source_value, object_path)
if hasattr(self, "_min") and (self._value < self._min):
raise ValueError(f"value {self._value} is lower than the minimum {self._min}.", object_path)
if hasattr(self, "_max") and (self._value > self._max):
raise ValueError(f"value {self._value} is higher than the maximum {self._max}", object_path)

@classmethod
def json_schema(cls: Type["FloatRangeBase"]) -> Dict[Any, Any]:
typ: Dict[str, Any] = {"type": "number"}
if hasattr(cls, "_min"):
typ["minimum"] = cls._min
if hasattr(cls, "_max"):
typ["maximum"] = cls._max
return typ


class PatternBase(StrBase):
"""
Base class to work with string value that match regex pattern.
Expand Down
13 changes: 12 additions & 1 deletion python/knot_resolver/datamodel/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
import re
from typing import Any, Dict, Optional, Type, Union

from knot_resolver.datamodel.types.base_types import IntRangeBase, PatternBase, StrBase, StringLengthBase, UnitBase
from knot_resolver.datamodel.types.base_types import (
FloatRangeBase,
IntRangeBase,
PatternBase,
StrBase,
StringLengthBase,
UnitBase,
)
from knot_resolver.utils.modeling import BaseValueType


Expand Down Expand Up @@ -46,6 +53,10 @@ def from_str(cls: Type["PortNumber"], port: str, object_path: str = "/") -> "Por
raise ValueError(f"invalid port number {port}") from e


class FloatNonNegative(FloatRangeBase):
_min: float = 0.0


class SizeUnit(UnitBase):
_units = {"B": 1, "K": 1024, "M": 1024**2, "G": 1024**3}

Expand Down
31 changes: 30 additions & 1 deletion tests/manager/datamodel/types/test_base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pytest import raises

from knot_resolver import KresBaseException
from knot_resolver.datamodel.types.base_types import IntRangeBase, StringLengthBase
from knot_resolver.datamodel.types.base_types import FloatRangeBase, IntRangeBase, StringLengthBase


@pytest.mark.parametrize("min,max", [(0, None), (None, 0), (1, 65535), (-65535, -1)])
Expand Down Expand Up @@ -38,6 +38,35 @@ class Test(IntRangeBase):
Test(inval)


@pytest.mark.parametrize("min,max", [(0.0, None), (None, 0.0), (1.0, 65535.0), (-65535.0, -1.0)])
def test_float_range_base(min: Optional[float], max: Optional[float]):
class Test(FloatRangeBase):
if min:
_min = min
if max:
_max = max

if min:
assert float(Test(min)) == min
if max:
assert float(Test(max)) == max

rmin = min if min else sys.float_info.min - 1.0
rmax = max if max else sys.float_info.max

n = 100
vals: List[float] = [random.uniform(rmin, rmax) for _ in range(n)]
assert [str(Test(val)) == f"{val}" for val in vals]

invals: List[float] = []
invals.extend([random.uniform(rmax + 1.0, sys.float_info.max) for _ in range(n % 2)] if max else [])
invals.extend([random.uniform(sys.float_info.min - 1.0, rmin - 1.0) for _ in range(n % 2)] if max else [])

for inval in invals:
with raises(KresBaseException):
Test(inval)


@pytest.mark.parametrize("min,max", [(10, None), (None, 10), (2, 32)])
def test_str_bytes_length_base(min: Optional[int], max: Optional[int]):
class Test(StringLengthBase):
Expand Down

0 comments on commit 906af8f

Please sign in to comment.