Skip to content

Commit

Permalink
Add decimal.Decimal initialization with tuple
Browse files Browse the repository at this point in the history
  • Loading branch information
y.kikvadze authored and yar-kik committed Feb 7, 2023
1 parent 60097c1 commit a3529da
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 22 deletions.
7 changes: 6 additions & 1 deletion conjector/type_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class TypeConverter:
pathlib.Path,
)

def cast_types(self, type_: Type, value: Any) -> Any:
def cast_types(self, type_: Union[Type, Any], value: Any) -> Any:
args = args if (args := get_args(type_)) else (Any,)
type_ = origin if (origin := get_origin(type_)) else type_
value = value if value != "null" else None
Expand Down Expand Up @@ -221,6 +221,11 @@ def _apply_enum(self, type_: Type[enum.Enum], values: Any) -> enum.Enum:
def _apply_decimal(self, value: Any) -> decimal.Decimal:
if value is None:
return decimal.Decimal()
if isinstance(value, list):
if len(value) != 3:
raise ValueError("Decimal value requires 3-items iterable!")
value = self.cast_types(Tuple[int, List[int], int], value)
return decimal.Decimal(value)
if self._is_number(value):
return decimal.Decimal(value)
raise ValueError(
Expand Down
40 changes: 20 additions & 20 deletions docs/supported_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@
`conjector` can work with deep nested data structures and recursively cast them to corresponding type hints.
The table below shows how config values (`json` syntax example) are cast to Python types:

| Python type | Config file type | Config example |
|----------------------------------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `int` | `int`<br/>`str` | `10`<br/>`"10"` |
| `float` | `float`<br/>`int`<br/>`str` | `10.5`<br/>`10`<br/>`"10.5"` |
| `str` | `str` | `"string value"` |
| `bool` | `bool`<br/>`int`<br/>`str` | `true` / `false`<br/>`1` / `0`<br/>`"True"` / `"False"`, `"true"` / `"false"` |
| `None` | `null` | `null` |
| `dict` | `dict` | `{"key": "value"}` |
| `list`<br/>`tuple`<br/>`set`<br/>`frozenset` | `list` | `["val1", "val2"]` |
| `TypedDict` | `dict` | `{"str_var": "value"}` |
| `NamedTuple` | `list`<br/>`dict` | `["value", 10]`<br/>`{"str_val": "value", "int_val": 10}` |
| `dataclass` | `dict` | `{"str_val": "str", "int_val": 10}` |
| `datetime.datetime` | `str`<br/>`int`<br/>`list`<br/>`dict` | `"2022-12-11T10:20:23"`<br/>`1670754600`<br/>`[2022, 12, 11, 10, 20, 23]`<br/>`{"year": 2022, "month": 12, "day": 11, "hour": 10, "minute": 20, "second": 23}` |
| `datetime.date` | `str`<br/>`list`<br/>`dict` | `"2022-12-11"`<br/>`[2022, 12, 11]`<br/>`{"year": 2022, "month": 12, "day": 11}` |
| `datetime.time` | `str`<br/>`list`<br/>`dict` | `"12:30:02"`<br/>`[12, 30, 2]`<br/>`{"hour": 12, "minute": 30, "second": 2}` |
| `datetime.timedelta` | `dict` | `{"days": 1, "hours": 2, "minutes": 10}` |
| `enum.Enum` | `str`<br/>`int` | `"VALUE"`<br/>`10` |
| `re.Pattern` | `str` | `"\w+"` |
| `decimal.Decimal` | `str`<br/>`int`<br/>`float` | `"12.150"`<br/>`100`<br/>`12.5` |
| `pathlib.Path` | `str` | `"some/path/to/file.txt"`/`"some/path/to/dir/"` |
| Python type | Config file type | Config example |
|----------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `int` | `int`<br/>`str` | `10`<br/>`"10"` |
| `float` | `float`<br/>`int`<br/>`str` | `10.5`<br/>`10`<br/>`"10.5"` |
| `str` | `str` | `"string value"` |
| `bool` | `bool`<br/>`int`<br/>`str` | `true` / `false`<br/>`1` / `0`<br/>`"True"` / `"False"`, `"true"` / `"false"` |
| `None` | `null` | `null` |
| `dict` | `dict` | `{"key": "value"}` |
| `list`<br/>`tuple`<br/>`set`<br/>`frozenset` | `list` | `["val1", "val2"]` |
| `TypedDict` | `dict` | `{"str_var": "value"}` |
| `NamedTuple` | `list`<br/>`dict` | `["value", 10]`<br/>`{"str_val": "value", "int_val": 10}` |
| `dataclass` | `dict` | `{"str_val": "str", "int_val": 10}` |
| `datetime.datetime` | `str`<br/>`int`<br/>`list`<br/>`dict` | `"2022-12-11T10:20:23"`<br/>`1670754600`<br/>`[2022, 12, 11, 10, 20, 23]`<br/>`{"year": 2022, "month": 12, "day": 11, "hour": 10, "minute": 20, "second": 23}` |
| `datetime.date` | `str`<br/>`list`<br/>`dict` | `"2022-12-11"`<br/>`[2022, 12, 11]`<br/>`{"year": 2022, "month": 12, "day": 11}` |
| `datetime.time` | `str`<br/>`list`<br/>`dict` | `"12:30:02"`<br/>`[12, 30, 2]`<br/>`{"hour": 12, "minute": 30, "second": 2}` |
| `datetime.timedelta` | `dict` | `{"days": 1, "hours": 2, "minutes": 10}` |
| `enum.Enum` | `str`<br/>`int` | `"VALUE"`<br/>`10` |
| `re.Pattern` | `str` | `"\w+"` |
| `decimal.Decimal` | `str`<br/>`int`<br/>`float`<br/>`list[int, list[int], int` | `"12.150"`<br/>`100`<br/>`12.5`<br/>`[1, [1, 2, 5], -3]` |
| `pathlib.Path` | `str` | `"some/path/to/file.txt"`/`"some/path/to/dir/"` |

## Optional types
The default behavior for the `Optional` type hint: try to convert the value to a specified type, if successful - use the converted value, else use None. Also, None will be used if no value is provided.
Expand Down
23 changes: 22 additions & 1 deletion tests/test_type_converter/test_decimal_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class DecimalClass:
int_decimal_var: Decimal
float_decimal_var: Decimal
str_decimal_var: Decimal
tuple_decimal_var: Decimal
missing_decimal_var: Decimal

return DecimalClass
Expand All @@ -28,13 +29,33 @@ def test_decimal_field_ok(decimal_class_fixt):
assert decimal_class_fixt.missing_decimal_var == Decimal()


@pytest.mark.parametrize(
"decimal_class_fixt",
("types_cast.yml", "types_cast.json", "types_cast.toml"),
indirect=True,
)
def test_from_tuple_decimal_field_ok(decimal_class_fixt):
assert decimal_class_fixt.tuple_decimal_var == Decimal("-0.125")


@pytest.mark.parametrize(
"filename",
("types_cast.yml", "types_cast.json", "types_cast.toml", "types_cast.ini"),
)
def test_invalid_decimal_field(filename):
def test_general_invalid_decimal_field(filename):
with pytest.raises(ValueError):

@properties(filename=filename, root="decimal")
class GeneralInvalidDecimalClass:
general_invalid_decimal_var: Decimal


@pytest.mark.parametrize(
"filename", ("types_cast.yml", "types_cast.json", "types_cast.toml")
)
def test_tuple_invalid_decimal_field(filename):
with pytest.raises(ValueError):

@properties(filename=filename, root="decimal")
class TupleInvalidDecimalClass:
tuple_invalid_decimal_var: Decimal
2 changes: 2 additions & 0 deletions tests/test_type_converter/types_cast.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@
"int_decimal_var": 10,
"float_decimal_var": 12.5,
"str_decimal_var": "15.150",
"tuple_decimal_var": [1, [1, 2, 5], -3],
"tuple_invalid_decimal_var": [1, 10],
"general_invalid_decimal_var": {"invalid": "value"}
},
"path": {
Expand Down
2 changes: 2 additions & 0 deletions tests/test_type_converter/types_cast.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ wrong_pattern_var = 10
int_decimal_var = 10
float_decimal_var = 12.5
str_decimal_var = "15.150"
tuple_decimal_var = [1, [1, 2, 5], -3]
tuple_invalid_decimal_var = [1, 10]
general_invalid_decimal_var = {invalid = "value"}

[path]
Expand Down
2 changes: 2 additions & 0 deletions tests/test_type_converter/types_cast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ decimal:
int_decimal_var: 10
float_decimal_var: 12.5
str_decimal_var: "15.150"
tuple_decimal_var: [1, [1, 2, 5], -3]
tuple_invalid_decimal_var: [1, 10]
general_invalid_decimal_var:
invalid: value

Expand Down

0 comments on commit a3529da

Please sign in to comment.