From a493d26df139b041f140b1a9dbbcd774fef6b968 Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Tue, 21 Nov 2023 14:57:10 +0900 Subject: [PATCH 01/12] Add support for Enums #29 --- src/dataclass_binder/_impl.py | 7 ++++- tests/test_formatting.py | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/dataclass_binder/_impl.py b/src/dataclass_binder/_impl.py index 1e7fb19..6eb6204 100644 --- a/src/dataclass_binder/_impl.py +++ b/src/dataclass_binder/_impl.py @@ -19,6 +19,7 @@ ) from dataclasses import MISSING, Field, asdict, dataclass, fields, is_dataclass, replace from datetime import date, datetime, time, timedelta +from enum import Enum from functools import reduce from importlib import import_module from inspect import cleandoc, get_annotations, getmodule, getsource, isabstract @@ -49,7 +50,7 @@ def _collect_type(field_type: type, context: str) -> type | Binder[Any]: return object elif not isinstance(field_type, type): raise TypeError(f"Annotation for field '{context}' is not a type") - elif issubclass(field_type, str | int | float | date | time | timedelta | ModuleType | Path): + elif issubclass(field_type, str | int | float | date | time | timedelta | ModuleType | Path | Enum): return field_type elif field_type is type: # https://github.com/python/mypy/issues/13026 @@ -314,6 +315,8 @@ def _bind_to_single_type(self, value: object, field_type: type, context: str) -> if not isinstance(value, str): raise TypeError(f"Expected TOML string for path '{context}', got '{type(value).__name__}'") return field_type(value) + elif issubclass(field_type, Enum): + return field_type(value) elif isinstance(value, field_type) and ( type(value) is not bool or field_type is bool or field_type is object ): @@ -697,6 +700,8 @@ def _to_toml_pair(value: object) -> tuple[str | None, Any]: return "-weeks", days // 7 else: return "-days", days + case Enum(): + return None, value.value case ModuleType(): return None, value.__name__ case Mapping(): diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 48d39c5..c17f1a1 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -3,6 +3,7 @@ from collections.abc import Sequence from dataclasses import dataclass, field from datetime import date, datetime, time, timedelta +from enum import Enum from io import BytesIO from pathlib import Path from types import ModuleType, NoneType, UnionType @@ -859,3 +860,57 @@ def test_format_template_no_module(sourceless_class: type[Any]) -> None: value = 0 """.strip() ) + + +class Color(Enum): + RED = "red" + BLUE = "blue" + GREEN = "green" + +class Number(Enum): + ONE = 1 + TWO = 2 + THREE = 3 + +@dataclass +class EnumEntry: + name: str + color: Color + number: Number + +def test_enums() -> None: + @dataclass + class Config: + best_colors: list[Color] + best_numbers: list[Number] + entries: list[EnumEntry] + + with stream_text( + """ + best-colors = ["red", "green", "blue"] + best-numbers = [1, 2, 3] + + [[entries]] + name = "Entry 1" + color = "blue" + number = 2 + + [[entries]] + name = "Entry 2" + color = "red" + number = 1 + """ + ) as stream: + config = Binder(Config).parse_toml(stream) + + assert len(config.best_colors) == 3 + assert len(config.best_numbers) == 3 + assert config.best_colors.index(Color.RED) == 0 + assert config.best_colors.index(Color.GREEN) == 1 + assert config.best_colors.index(Color.BLUE) == 2 + assert all(num in config.best_numbers for num in Number) + assert len(config.entries) == 2 + assert config.entries[0].color == Color.BLUE + assert config.entries[0].number == Number.TWO + assert config.entries[1].color == Color.RED + assert config.entries[1].number == Number.ONE From 4c06b3102f328221eca5602788ba53994a1a8be9 Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Tue, 21 Nov 2023 15:25:42 +0900 Subject: [PATCH 02/12] Add formatting test and move parsing test into correct file --- tests/test_formatting.py | 66 +++++++++++----------------------------- tests/test_parsing.py | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 48 deletions(-) diff --git a/tests/test_formatting.py b/tests/test_formatting.py index c17f1a1..cba7c06 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -862,55 +862,25 @@ def test_format_template_no_module(sourceless_class: type[Any]) -> None: ) -class Color(Enum): - RED = "red" - BLUE = "blue" - GREEN = "green" +class IssueStatus(Enum): + OPEN = "open" + REJECTED = "rejected" + COMPLETED = "completed" -class Number(Enum): - ONE = 1 - TWO = 2 - THREE = 3 -@dataclass -class EnumEntry: - name: str - color: Color - number: Number - -def test_enums() -> None: +def test_format_with_enums() -> None: @dataclass - class Config: - best_colors: list[Color] - best_numbers: list[Number] - entries: list[EnumEntry] + class Issue: + id: int + title: str + status: IssueStatus - with stream_text( - """ - best-colors = ["red", "green", "blue"] - best-numbers = [1, 2, 3] - - [[entries]] - name = "Entry 1" - color = "blue" - number = 2 - - [[entries]] - name = "Entry 2" - color = "red" - number = 1 - """ - ) as stream: - config = Binder(Config).parse_toml(stream) - - assert len(config.best_colors) == 3 - assert len(config.best_numbers) == 3 - assert config.best_colors.index(Color.RED) == 0 - assert config.best_colors.index(Color.GREEN) == 1 - assert config.best_colors.index(Color.BLUE) == 2 - assert all(num in config.best_numbers for num in Number) - assert len(config.entries) == 2 - assert config.entries[0].color == Color.BLUE - assert config.entries[0].number == Number.TWO - assert config.entries[1].color == Color.RED - assert config.entries[1].number == Number.ONE + issue = Issue(1, "Test", IssueStatus.OPEN) + + template = "\n".join(Binder(issue).format_toml()) + + assert template == """ +id = 1 +title = 'Test' +status = 'open' +""".strip() diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 79a4a93..68f1aa9 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -5,6 +5,7 @@ from contextlib import contextmanager from dataclasses import FrozenInstanceError, dataclass, field from datetime import date, datetime, time, timedelta +from enum import Enum from io import BytesIO from pathlib import Path from types import ModuleType @@ -1084,3 +1085,60 @@ def test_bind_merge() -> None: assert merged_config.flag is True assert merged_config.nested1.value == "sun" assert merged_config.nested2.value == "cheese" + + +class Color(Enum): + RED = "red" + BLUE = "blue" + GREEN = "green" + + +class Number(Enum): + ONE = 1 + TWO = 2 + THREE = 3 + + +@dataclass +class EnumEntry: + name: str + color: Color + number: Number + + +def test_enums() -> None: + @dataclass + class Config: + best_colors: list[Color] + best_numbers: list[Number] + entries: list[EnumEntry] + + with stream_text( + """ + best-colors = ["red", "green", "blue"] + best-numbers = [1, 2, 3] + + [[entries]] + name = "Entry 1" + color = "blue" + number = 2 + + [[entries]] + name = "Entry 2" + color = "red" + number = 1 + """ + ) as stream: + config = Binder(Config).parse_toml(stream) + + assert len(config.best_colors) == 3 + assert len(config.best_numbers) == 3 + assert config.best_colors.index(Color.RED) == 0 + assert config.best_colors.index(Color.GREEN) == 1 + assert config.best_colors.index(Color.BLUE) == 2 + assert all(num in config.best_numbers for num in Number) + assert len(config.entries) == 2 + assert config.entries[0].color == Color.BLUE + assert config.entries[0].number == Number.TWO + assert config.entries[1].color == Color.RED + assert config.entries[1].number == Number.ONE From 5afa68653d7943c85f1788d9256982264ffb5f67 Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Tue, 21 Nov 2023 15:30:45 +0900 Subject: [PATCH 03/12] Fix test class shadowing id --- tests/test_formatting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_formatting.py b/tests/test_formatting.py index cba7c06..289ca8d 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -871,7 +871,7 @@ class IssueStatus(Enum): def test_format_with_enums() -> None: @dataclass class Issue: - id: int + issue_id: int title: str status: IssueStatus @@ -880,7 +880,7 @@ class Issue: template = "\n".join(Binder(issue).format_toml()) assert template == """ -id = 1 +issue-id = 1 title = 'Test' status = 'open' """.strip() From 3da4c2c7a71b130df4bfbf311f1e8aae9c84b427 Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Tue, 21 Nov 2023 15:43:37 +0900 Subject: [PATCH 04/12] Make formatter happy --- tests/test_formatting.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 289ca8d..662848b 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -879,8 +879,10 @@ class Issue: template = "\n".join(Binder(issue).format_toml()) - assert template == """ + assert template == ( + """ issue-id = 1 title = 'Test' status = 'open' """.strip() + ) From 0da89a57d7877b41dbf08e60f59e6233f3dc97d7 Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Thu, 23 Nov 2023 10:34:32 +0900 Subject: [PATCH 05/12] adjust unit tests --- src/dataclass_binder/_impl.py | 1 - tests/test_formatting.py | 34 ++++++++++++++++++++-------------- tests/test_parsing.py | 18 +++++++++--------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/dataclass_binder/_impl.py b/src/dataclass_binder/_impl.py index 6eb6204..456ea1f 100644 --- a/src/dataclass_binder/_impl.py +++ b/src/dataclass_binder/_impl.py @@ -210,7 +210,6 @@ def _check_field(field: Field, field_type: type, context: str) -> None: @dataclass(slots=True) class _ClassInfo(Generic[T]): - _cache: ClassVar[MutableMapping[type[Any], _ClassInfo[Any]]] = WeakKeyDictionary() dataclass: type[T] diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 662848b..5a1892e 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -3,7 +3,7 @@ from collections.abc import Sequence from dataclasses import dataclass, field from datetime import date, datetime, time, timedelta -from enum import Enum +from enum import Enum, auto, IntEnum from io import BytesIO from pathlib import Path from types import ModuleType, NoneType, UnionType @@ -862,27 +862,33 @@ def test_format_template_no_module(sourceless_class: type[Any]) -> None: ) -class IssueStatus(Enum): - OPEN = "open" - REJECTED = "rejected" - COMPLETED = "completed" +class Verbosity(Enum): + QUIET = auto() + NORMAL = auto() + DETAILED = auto() + + +class IntVerbosity(IntEnum): + QUIET = 0 + NORMAL = 1 + DETAILED = 2 def test_format_with_enums() -> None: @dataclass - class Issue: - issue_id: int - title: str - status: IssueStatus + class Log: + message: str + verbosity: Verbosity + verbosity_level: IntVerbosity - issue = Issue(1, "Test", IssueStatus.OPEN) + log = Log("Hello, World", Verbosity.DETAILED, IntVerbosity.DETAILED) - template = "\n".join(Binder(issue).format_toml()) + template = "\n".join(Binder(log).format_toml()) assert template == ( """ -issue-id = 1 -title = 'Test' -status = 'open' +message = "Hello, World" +verbosity = "detailed" +verbosity-evel = 2 """.strip() ) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 68f1aa9..f678985 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from dataclasses import FrozenInstanceError, dataclass, field from datetime import date, datetime, time, timedelta -from enum import Enum +from enum import Enum, IntEnum from io import BytesIO from pathlib import Path from types import ModuleType @@ -1088,12 +1088,12 @@ def test_bind_merge() -> None: class Color(Enum): - RED = "red" - BLUE = "blue" - GREEN = "green" + RED = "#FF0000" + GREEN = "#00FF00" + BLUE = "#0000FF" -class Number(Enum): +class Number(IntEnum): ONE = 1 TWO = 2 THREE = 3 @@ -1138,7 +1138,7 @@ class Config: assert config.best_colors.index(Color.BLUE) == 2 assert all(num in config.best_numbers for num in Number) assert len(config.entries) == 2 - assert config.entries[0].color == Color.BLUE - assert config.entries[0].number == Number.TWO - assert config.entries[1].color == Color.RED - assert config.entries[1].number == Number.ONE + assert config.entries[0].color is Color.BLUE + assert config.entries[0].number is Number.TWO + assert config.entries[1].color is Color.RED + assert config.entries[1].number is Number.ONE From 219fff34141b6ab3d1e2ff4d9daa1827babc0c45 Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Thu, 23 Nov 2023 11:01:52 +0900 Subject: [PATCH 06/12] use enum key to represent Enums, but use value based approach for ReprEnums --- src/dataclass_binder/_impl.py | 15 ++++++++++++--- tests/test_formatting.py | 8 ++++---- tests/test_parsing.py | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/dataclass_binder/_impl.py b/src/dataclass_binder/_impl.py index 456ea1f..9bedcf6 100644 --- a/src/dataclass_binder/_impl.py +++ b/src/dataclass_binder/_impl.py @@ -19,7 +19,7 @@ ) from dataclasses import MISSING, Field, asdict, dataclass, fields, is_dataclass, replace from datetime import date, datetime, time, timedelta -from enum import Enum +from enum import Enum, ReprEnum from functools import reduce from importlib import import_module from inspect import cleandoc, get_annotations, getmodule, getsource, isabstract @@ -50,7 +50,7 @@ def _collect_type(field_type: type, context: str) -> type | Binder[Any]: return object elif not isinstance(field_type, type): raise TypeError(f"Annotation for field '{context}' is not a type") - elif issubclass(field_type, str | int | float | date | time | timedelta | ModuleType | Path | Enum): + elif issubclass(field_type, str | int | float | date | time | timedelta | ModuleType | Path | Enum | ReprEnum): return field_type elif field_type is type: # https://github.com/python/mypy/issues/13026 @@ -314,8 +314,15 @@ def _bind_to_single_type(self, value: object, field_type: type, context: str) -> if not isinstance(value, str): raise TypeError(f"Expected TOML string for path '{context}', got '{type(value).__name__}'") return field_type(value) - elif issubclass(field_type, Enum): + elif issubclass(field_type, ReprEnum): return field_type(value) + elif issubclass(field_type, Enum): + if not isinstance(value, str): + raise TypeError(f"'{value}' is not a valid key for enum '{field_type}', must be of type str") + for enum_value in field_type: + if enum_value.name.lower() == value.lower(): + return enum_value + raise TypeError(f"'{value}' is not a valid key for enum '{field_type}', could not be found") elif isinstance(value, field_type) and ( type(value) is not bool or field_type is bool or field_type is object ): @@ -700,6 +707,8 @@ def _to_toml_pair(value: object) -> tuple[str | None, Any]: else: return "-days", days case Enum(): + return None, value.name.lower() + case ReprEnum(): return None, value.value case ModuleType(): return None, value.__name__ diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 5a1892e..2057276 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -3,7 +3,7 @@ from collections.abc import Sequence from dataclasses import dataclass, field from datetime import date, datetime, time, timedelta -from enum import Enum, auto, IntEnum +from enum import Enum, IntEnum, auto from io import BytesIO from pathlib import Path from types import ModuleType, NoneType, UnionType @@ -887,8 +887,8 @@ class Log: assert template == ( """ -message = "Hello, World" -verbosity = "detailed" -verbosity-evel = 2 +message = 'Hello, World' +verbosity = 'detailed' +verbosity-level = 2 """.strip() ) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index f678985..db29ae8 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1142,3 +1142,19 @@ class Config: assert config.entries[0].number is Number.TWO assert config.entries[1].color is Color.RED assert config.entries[1].number is Number.ONE + + +def test_key_based_enum_while_using_value_ident() -> None: + @dataclass + class UserColorPreference: + primary: Color + secondary: Color + + with stream_text( + """ + primary = "#FF0000" + seconadry = "blue" + """ + ) as stream: + with pytest.raises(TypeError): + Binder(UserColorPreference).parse_toml(stream) From 9dc36a48a74e575ffed465deae7eab03f4190b90 Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Thu, 23 Nov 2023 11:12:32 +0900 Subject: [PATCH 07/12] add more tests --- tests/test_parsing.py | 47 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index db29ae8..53d62d1 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1144,6 +1144,42 @@ class Config: assert config.entries[1].number is Number.ONE +def test_enum_with_invalid_value() -> None: + @dataclass + class UserFavorites: + favorite_number: Number + favorite_color: Color + + with stream_text( + """ + favorite-number = "one" + favorite-color = "red" + """ + ) as stream, pytest.raises(ValueError): # noqa: PT011 + Binder(UserFavorites).parse_toml(stream) + + +def test_enum_keys_being_case_insensitive() -> None: + @dataclass + class Theme: + primary: Color + secondary: Color + accent: Color + + with stream_text( + """ + primary = "RED" + secondary = "green" + accent = "blUE" + """ + ) as stream: + theme = Binder(Theme).parse_toml(stream) + + assert theme.primary is Color.RED + assert theme.secondary is Color.GREEN + assert theme.accent is Color.BLUE + + def test_key_based_enum_while_using_value_ident() -> None: @dataclass class UserColorPreference: @@ -1152,9 +1188,8 @@ class UserColorPreference: with stream_text( """ - primary = "#FF0000" - seconadry = "blue" - """ - ) as stream: - with pytest.raises(TypeError): - Binder(UserColorPreference).parse_toml(stream) + primary = "#FF0000" + seconadry = "blue" + """ + ) as stream, pytest.raises(TypeError): + Binder(UserColorPreference).parse_toml(stream) From f3485a9a610383b318a9444c407ae27809c921d5 Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Mon, 18 Dec 2023 10:56:59 +0900 Subject: [PATCH 08/12] add ReprEnum workaround for Python 3.10 --- src/dataclass_binder/_impl.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/dataclass_binder/_impl.py b/src/dataclass_binder/_impl.py index 9bedcf6..1a11782 100644 --- a/src/dataclass_binder/_impl.py +++ b/src/dataclass_binder/_impl.py @@ -19,7 +19,7 @@ ) from dataclasses import MISSING, Field, asdict, dataclass, fields, is_dataclass, replace from datetime import date, datetime, time, timedelta -from enum import Enum, ReprEnum +from enum import Enum from functools import reduce from importlib import import_module from inspect import cleandoc, get_annotations, getmodule, getsource, isabstract @@ -31,7 +31,19 @@ if sys.version_info < (3, 11): import tomli as tomllib # pragma: no cover + + if TYPE_CHECKING: + + class ReprEnum(Enum): + ... + + else: + from enum import IntEnum, IntFlag + + ReprEnum = IntEnum | IntFlag else: + from enum import ReprEnum + import tomllib # pragma: no cover @@ -707,9 +719,10 @@ def _to_toml_pair(value: object) -> tuple[str | None, Any]: else: return "-days", days case Enum(): - return None, value.name.lower() - case ReprEnum(): - return None, value.value + if isinstance(value, ReprEnum): + return None, value.value + else: + return None, value.name.lower() case ModuleType(): return None, value.__name__ case Mapping(): From a4ba84cdd236b4b1c9090705faf17a68d2112b78 Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Mon, 18 Dec 2023 11:08:46 +0900 Subject: [PATCH 09/12] add missing test case --- tests/test_parsing.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 53d62d1..08347bd 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1099,6 +1099,16 @@ class Number(IntEnum): THREE = 3 +class Weekday(Enum): + MONDAY = 0 + TUESDAY = 1 + WEDNESDAY = 2 + THURSDAY = 3 + FRIDAY = 4 + SATURDAY = 5 + SUNDAY = 6 + + @dataclass class EnumEntry: name: str @@ -1193,3 +1203,26 @@ class UserColorPreference: """ ) as stream, pytest.raises(TypeError): Binder(UserColorPreference).parse_toml(stream) + + +def test_enum_parsing_with_invalid_key_type() -> None: + @dataclass + class UserPrefs: + name: str + start_of_the_week: Weekday + + with stream_text( + """ + name = "Peter Testuser" + start-of-the-week = "sunday" + """ + ) as stream: + Binder(UserPrefs).parse_toml(stream) + + with stream_text( + """ + name = "Peter Testuser" + start-of-the-week = 1 + """ + ) as stream, pytest.raises(TypeError): + Binder(UserPrefs).parse_toml(stream) From 56b051d42e38c8ac1f1b54772a380db1ef03db9d Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Mon, 18 Dec 2023 11:36:07 +0900 Subject: [PATCH 10/12] fix enum related formatting bug in Python 3.10 --- src/dataclass_binder/_impl.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/dataclass_binder/_impl.py b/src/dataclass_binder/_impl.py index 1a11782..ba34c74 100644 --- a/src/dataclass_binder/_impl.py +++ b/src/dataclass_binder/_impl.py @@ -689,6 +689,13 @@ def format_toml_pair(key: str, value: object) -> str: def _to_toml_pair(value: object) -> tuple[str | None, Any]: """Return a TOML-compatible suffix and value pair with the data from the given rich value object.""" match value: + # enums have to be checked before basic types because for instance + # IntEnum is also of type int + case Enum(): + if isinstance(value, ReprEnum): + return None, value.value + else: + return None, value.name.lower() case str() | int() | float() | date() | time() | Path(): # note: 'bool' is a subclass of 'int' return None, value case timedelta(): @@ -718,11 +725,6 @@ def _to_toml_pair(value: object) -> tuple[str | None, Any]: return "-weeks", days // 7 else: return "-days", days - case Enum(): - if isinstance(value, ReprEnum): - return None, value.value - else: - return None, value.name.lower() case ModuleType(): return None, value.__name__ case Mapping(): From 1bbdda505197193d3346c774c3dd701093c1e89e Mon Sep 17 00:00:00 2001 From: Maarten ter Huurne Date: Thu, 25 Jan 2024 12:07:37 +0100 Subject: [PATCH 11/12] Work around formatting inconsistency between isort and Ruff --- src/dataclass_binder/_impl.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/dataclass_binder/_impl.py b/src/dataclass_binder/_impl.py index ba34c74..29127e8 100644 --- a/src/dataclass_binder/_impl.py +++ b/src/dataclass_binder/_impl.py @@ -29,8 +29,8 @@ from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, Generic, TypeVar, Union, cast, get_args, get_origin, overload from weakref import WeakKeyDictionary -if sys.version_info < (3, 11): - import tomli as tomllib # pragma: no cover +if sys.version_info < (3, 11): # pragma: no cover + import tomli as tomllib if TYPE_CHECKING: @@ -41,11 +41,10 @@ class ReprEnum(Enum): from enum import IntEnum, IntFlag ReprEnum = IntEnum | IntFlag -else: +else: # pragma: no cover + import tomllib # noqa: I001 from enum import ReprEnum - import tomllib # pragma: no cover - def _collect_type(field_type: type, context: str) -> type | Binder[Any]: """ From a5a70b6de22d61be8a6cf5cc834313f351c9236d Mon Sep 17 00:00:00 2001 From: Christopher Kaster Date: Wed, 31 Jan 2024 11:22:11 +0100 Subject: [PATCH 12/12] fix remarks from code review --- src/dataclass_binder/_impl.py | 15 ++++++++++++--- tests/test_parsing.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/dataclass_binder/_impl.py b/src/dataclass_binder/_impl.py index 9eb9555..8c94883 100644 --- a/src/dataclass_binder/_impl.py +++ b/src/dataclass_binder/_impl.py @@ -61,7 +61,7 @@ def _collect_type(field_type: type, context: str) -> type | Binder[Any]: return object elif not isinstance(field_type, type): raise TypeError(f"Annotation for field '{context}' is not a type") - elif issubclass(field_type, str | int | float | date | time | timedelta | ModuleType | Path | Enum | ReprEnum): + elif issubclass(field_type, str | int | float | date | time | timedelta | ModuleType | Path | Enum): return field_type elif field_type is type: # https://github.com/python/mypy/issues/13026 @@ -326,14 +326,23 @@ def _bind_to_single_type(self, value: object, field_type: type, context: str) -> raise TypeError(f"Expected TOML string for path '{context}', got '{type(value).__name__}'") return field_type(value) elif issubclass(field_type, ReprEnum): + if issubclass(field_type, int) and not isinstance(value, int): + raise TypeError(f"Value for '{context}': '{value}' is not of type int") + if issubclass(field_type, str) and not isinstance(value, str): + raise TypeError(f"Value for '{context}': '{value}' is not of type str") return field_type(value) elif issubclass(field_type, Enum): if not isinstance(value, str): - raise TypeError(f"'{value}' is not a valid key for enum '{field_type}', must be of type str") + raise TypeError( + f"Value for '{context}': '{value}' is not a valid key for enum '{field_type}', " + f"must be of type str" + ) for enum_value in field_type: if enum_value.name.lower() == value.lower(): return enum_value - raise TypeError(f"'{value}' is not a valid key for enum '{field_type}', could not be found") + raise TypeError( + f"Value for '{context}': '{value}' is not a valid key for enum '{field_type}', could not be found" + ) elif isinstance(value, field_type) and ( type(value) is not bool or field_type is bool or field_type is object ): diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 08347bd..28e4495 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1165,7 +1165,7 @@ class UserFavorites: favorite-number = "one" favorite-color = "red" """ - ) as stream, pytest.raises(ValueError): # noqa: PT011 + ) as stream, pytest.raises(TypeError): Binder(UserFavorites).parse_toml(stream)