Skip to content

Commit a24f1e2

Browse files
authored
Add enum type conversion tests and update data_types.py for enum handling (#59)
1 parent 2ddb298 commit a24f1e2

3 files changed

Lines changed: 159 additions & 14 deletions

File tree

ruff.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ target-version = "py311"
66
[lint.per-file-ignores]
77
"test_*.py" = ["S101", "D103", "SLF001", "D100","D101", "FBT003", "PLR2004", "N814", "S105"]
88
"examples/*.py" = ["ALL"]
9+
"src/confkit/data_types.py" = ["E701"] # Allow inline return after : on match for readability
910

1011
[format]
1112
# Don't apply line-length formatting to comments

src/confkit/data_types.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from abc import ABC, abstractmethod
77
from collections.abc import Sequence
88
from datetime import UTC, date, datetime, time, timedelta, tzinfo
9+
from enum import IntEnum as dIntEnum
10+
from enum import IntFlag as dIntFlag
11+
from enum import StrEnum as dStrEnum
12+
from enum import Enum as dEnum
913
from typing import ClassVar, Generic, NotRequired, Required, TypedDict, TypeVar, Unpack, cast, overload
1014

1115
from confkit.sentinels import UNSET
@@ -65,30 +69,28 @@ def cast_optional(default: T | None | BaseDataType[T]) -> BaseDataType[T | None]
6569
return Optional(BaseDataType.cast(default))
6670

6771
@staticmethod
68-
def cast(default: T | BaseDataType[T]) -> BaseDataType[T]:
72+
def cast(default: T | BaseDataType[T]) -> BaseDataType[T]: # noqa: C901, PLR0911
6973
"""Convert the default value to a BaseDataType."""
7074
# We use Cast to shut up type checkers, as we know primitive types will be correct.
7175
# If a custom type is passed, it should be a BaseDataType subclass, which already has the correct types.
76+
# Check enum types BEFORE basic types since some enums inherit from str/int
7277
match default:
73-
case bool():
74-
data_type = cast("BaseDataType[T]", Boolean(default))
75-
case None:
76-
data_type = cast("BaseDataType[T]", NoneType())
77-
case int():
78-
data_type = cast("BaseDataType[T]", Integer(default))
79-
case float():
80-
data_type = cast("BaseDataType[T]", Float(default))
81-
case str():
82-
data_type = cast("BaseDataType[T]", String(default))
83-
case BaseDataType():
84-
data_type = default
78+
case dStrEnum(): return cast("BaseDataType[T]", StrEnum(default))
79+
case dIntFlag(): return cast("BaseDataType[T]", IntFlag(default))
80+
case dIntEnum(): return cast("BaseDataType[T]", IntEnum(default))
81+
case dEnum(): return cast("BaseDataType[T]", Enum(default))
82+
case bool(): return cast("BaseDataType[T]", Boolean(default))
83+
case None: return cast("BaseDataType[T]", NoneType())
84+
case int(): return cast("BaseDataType[T]", Integer(default))
85+
case float(): return cast("BaseDataType[T]", Float(default))
86+
case str(): return cast("BaseDataType[T]", String(default))
87+
case BaseDataType(): return default
8588
case _:
8689
msg = (
8790
f"Unsupported default value type: {type(default).__name__}. "
8891
"Use a BaseDataType subclass for custom types."
8992
)
9093
raise InvalidDefaultError(msg)
91-
return data_type
9294

9395

9496
class _EnumBase(BaseDataType[T]):

tests/test_enum_auto_conversion.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Tests for automatic enum type conversion in Config descriptors.
2+
3+
Tests that StrEnum, IntEnum, IntFlag, and Enum defaults are automatically
4+
wrapped in their corresponding data type converters when used with Config.
5+
"""
6+
7+
import enum
8+
from enum import IntEnum, IntFlag, StrEnum
9+
from pathlib import Path
10+
11+
from confkit import Config as ConfigBase
12+
from confkit.data_types import Enum as ConfigEnum
13+
from confkit.data_types import IntEnum as ConfigIntEnum
14+
from confkit.data_types import IntFlag as ConfigIntFlag
15+
from confkit.data_types import StrEnum as ConfigStrEnum
16+
from confkit.parsers import IniParser
17+
18+
19+
class LogLevel(StrEnum):
20+
"""String-based enum for log levels."""
21+
22+
DEBUG = "debug"
23+
INFO = "info"
24+
WARNING = "warning"
25+
ERROR = "error"
26+
27+
28+
class Priority(IntEnum):
29+
"""Integer-based enum for task priorities."""
30+
31+
LOW = 0
32+
MEDIUM = 5
33+
HIGH = 10
34+
35+
36+
class Permission(IntFlag):
37+
"""Integer flag enum for permission bits."""
38+
39+
NONE = 0
40+
READ = 1
41+
WRITE = 2
42+
EXECUTE = 4
43+
ALL = READ | WRITE | EXECUTE
44+
45+
46+
class StandardEnum(enum.Enum):
47+
"""Standard enum for testing."""
48+
49+
OPTION_A = 1
50+
OPTION_B = 2
51+
52+
53+
class TestStrEnumAutoWrapping:
54+
"""Test that StrEnum instances are automatically wrapped."""
55+
56+
def test_strenum_auto_wraps_to_config_strenum(self, tmp_path: Path) -> None:
57+
"""Test that Config(StrEnum.value) auto-wraps to Config(ConfigStrEnum(StrEnum.value))."""
58+
config_file = tmp_path / "config.ini"
59+
config_file.write_text("")
60+
61+
class StrEnumConfig(ConfigBase):
62+
pass
63+
64+
StrEnumConfig.set_parser(IniParser())
65+
StrEnumConfig.set_file(config_file)
66+
StrEnumConfig._has_read_config = False
67+
68+
class AppConfig:
69+
log_level = StrEnumConfig(LogLevel.INFO)
70+
71+
descriptor = AppConfig.__dict__["log_level"]
72+
assert isinstance(descriptor._data_type, ConfigStrEnum)
73+
assert descriptor._data_type.value == LogLevel.INFO
74+
75+
76+
class TestIntEnumAutoWrapping:
77+
"""Test that IntEnum instances are automatically wrapped."""
78+
79+
def test_intenum_auto_wraps_to_config_intenum(self, tmp_path: Path) -> None:
80+
"""Test that Config(IntEnum.value) auto-wraps to Config(ConfigIntEnum(IntEnum.value))."""
81+
config_file = tmp_path / "config.ini"
82+
config_file.write_text("")
83+
84+
class IntEnumConfig(ConfigBase):
85+
pass
86+
87+
IntEnumConfig.set_parser(IniParser())
88+
IntEnumConfig.set_file(config_file)
89+
IntEnumConfig._has_read_config = False
90+
91+
class AppConfig:
92+
priority = IntEnumConfig(Priority.MEDIUM)
93+
94+
descriptor = AppConfig.__dict__["priority"]
95+
assert isinstance(descriptor._data_type, ConfigIntEnum)
96+
assert descriptor._data_type.value == Priority.MEDIUM
97+
98+
99+
class TestIntFlagAutoWrapping:
100+
"""Test that IntFlag instances are automatically wrapped."""
101+
102+
def test_intflag_auto_wraps_to_config_intflag(self, tmp_path: Path) -> None:
103+
"""Test that Config(IntFlag.value) auto-wraps to Config(ConfigIntFlag(IntFlag.value))."""
104+
config_file = tmp_path / "config.ini"
105+
config_file.write_text("")
106+
107+
class IntFlagConfig(ConfigBase):
108+
pass
109+
110+
IntFlagConfig.set_parser(IniParser())
111+
IntFlagConfig.set_file(config_file)
112+
IntFlagConfig._has_read_config = False
113+
114+
class AppConfig:
115+
perms = IntFlagConfig(Permission.READ)
116+
117+
descriptor = AppConfig.__dict__["perms"]
118+
assert isinstance(descriptor._data_type, ConfigIntFlag)
119+
assert descriptor._data_type.value == Permission.READ
120+
121+
122+
class TestStandardEnumAutoWrapping:
123+
"""Test that standard Enum instances are automatically wrapped."""
124+
125+
def test_standard_enum_auto_wraps_to_config_enum(self, tmp_path: Path) -> None:
126+
"""Test that Config(Enum.value) auto-wraps to Config(ConfigEnum(Enum.value))."""
127+
config_file = tmp_path / "config.ini"
128+
config_file.write_text("")
129+
130+
class EnumConfig(ConfigBase):
131+
pass
132+
133+
EnumConfig.set_parser(IniParser())
134+
EnumConfig.set_file(config_file)
135+
EnumConfig._has_read_config = False
136+
137+
class AppConfig:
138+
option = EnumConfig(StandardEnum.OPTION_A)
139+
140+
descriptor = AppConfig.__dict__["option"]
141+
assert isinstance(descriptor._data_type, ConfigEnum)
142+
assert descriptor._data_type.value == StandardEnum.OPTION_A

0 commit comments

Comments
 (0)