-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #105 from uclahs-cds/aholmes-fix-config-bug
Fix confusing issue when config class names don't end with `Config`.
- Loading branch information
Showing
3 changed files
with
154 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
src/programming/BL_Python/programming/config/exceptions.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
class InvalidConfigNameError(Exception): | ||
"""The class name used as a configuration type is invalid.""" | ||
|
||
|
||
class NotEndsWithConfigError(InvalidConfigNameError): | ||
"""The name must end with `Config`.""" | ||
|
||
|
||
class ConfigBuilderStateError(Exception): | ||
"""The config builder has not been configured correctly.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import pytest | ||
from BL_Python.programming.config import AbstractConfig, ConfigBuilder, load_config | ||
from BL_Python.programming.config.exceptions import ( | ||
ConfigBuilderStateError, | ||
NotEndsWithConfigError, | ||
) | ||
from pydantic import BaseModel | ||
from pytest_mock import MockerFixture | ||
|
||
|
||
class FooConfig(BaseModel): | ||
value: str | ||
other_value: bool = False | ||
|
||
|
||
class BarConfig(BaseModel): | ||
value: str | ||
|
||
|
||
class BazConfig(BaseModel, AbstractConfig): | ||
value: str | ||
|
||
|
||
class TestConfig(BaseModel, AbstractConfig): | ||
foo: FooConfig = FooConfig(value="xyz") | ||
bar: BarConfig | None = None | ||
|
||
|
||
class InvalidConfigClass(BaseModel, AbstractConfig): | ||
pass | ||
|
||
|
||
def test__Config__load_config__reads_toml_file(mocker: MockerFixture): | ||
fake_config_dict = {} | ||
toml_mock = mocker.patch("toml.load", return_value=fake_config_dict) | ||
_ = load_config(TestConfig, "foo.toml") | ||
assert toml_mock.called | ||
|
||
|
||
def test__Config__load_config__initializes_section_config_value(mocker: MockerFixture): | ||
fake_config_dict = {"foo": {"value": "abc123"}} | ||
_ = mocker.patch("io.open") | ||
_ = mocker.patch("toml.decoder.loads", return_value=fake_config_dict) | ||
config = load_config(TestConfig, "foo.toml") | ||
assert config.foo.value == "abc123" | ||
|
||
|
||
def test__Config__load_config__initializes_section_config(mocker: MockerFixture): | ||
fake_config_dict = {"bar": {"value": "abc123"}} | ||
_ = mocker.patch("io.open") | ||
_ = mocker.patch("toml.decoder.loads", return_value=fake_config_dict) | ||
config = load_config(TestConfig, "foo.toml") | ||
assert config.bar is not None | ||
assert config.bar.value == "abc123" | ||
|
||
|
||
def test__Config__load_config__applies_overrides(mocker: MockerFixture): | ||
fake_config_dict = {"foo": {"value": "abc123"}} | ||
override_config_dict = {"foo": {"value": "XYZ"}} | ||
_ = mocker.patch("io.open") | ||
_ = mocker.patch("toml.decoder.loads", return_value=fake_config_dict) | ||
config = load_config(TestConfig, "foo.toml", override_config_dict) | ||
assert config.foo.value == override_config_dict["foo"]["value"] | ||
|
||
|
||
def test__ConfigBuilder__build__raises_error_when_no_root_config_and_no_section_configs_specified(): | ||
config_builder = ConfigBuilder[TestConfig]() | ||
with pytest.raises(ConfigBuilderStateError): | ||
_ = config_builder.build() | ||
|
||
|
||
def test__ConfigBuilder__build__raises_error_when_section_class_name_is_invalid(): | ||
config_builder = ConfigBuilder[TestConfig]() | ||
_ = config_builder.with_configs([InvalidConfigClass]) | ||
with pytest.raises(NotEndsWithConfigError): | ||
_ = config_builder.build() | ||
|
||
|
||
def test__ConfigBuilder__build__uses_object_as_root_config_when_no_root_config_specified(): | ||
config_builder = ConfigBuilder[TestConfig]() | ||
_ = config_builder.with_configs([BazConfig]) | ||
config_type = config_builder.build() | ||
assert TestConfig not in config_type.__mro__ | ||
assert BazConfig not in config_type.__mro__ | ||
assert hasattr(config_type, "baz") | ||
assert hasattr(config_type(), "baz") | ||
|
||
|
||
def test__ConfigBuilder__build__uses_root_config_when_no_section_configs_specified(): | ||
config_builder = ConfigBuilder[TestConfig]() | ||
_ = config_builder.with_root_config(TestConfig) | ||
config_type = config_builder.build() | ||
assert config_type is TestConfig | ||
assert isinstance(config_type(), TestConfig) | ||
|
||
|
||
def test__ConfigBuilder__build__creates_config_type_when_multiple_configs_specified( | ||
mocker: MockerFixture, | ||
): | ||
fake_config_dict = {"baz": {"value": "ABC"}} | ||
_ = mocker.patch("io.open") | ||
_ = mocker.patch("toml.decoder.loads", return_value=fake_config_dict) | ||
|
||
config_builder = ConfigBuilder[TestConfig]() | ||
_ = config_builder.with_root_config(TestConfig) | ||
_ = config_builder.with_configs([BazConfig]) | ||
config_type = config_builder.build() | ||
config = load_config(config_type, "foo.toml") | ||
|
||
assert TestConfig in config_type.__mro__ | ||
assert hasattr(config, "baz") | ||
|
||
|
||
def test__ConfigBuilder__build__sets_dynamic_config_values_when_multiple_configs_specified( | ||
mocker: MockerFixture, | ||
): | ||
fake_config_dict = {"baz": {"value": "ABC"}} | ||
_ = mocker.patch("io.open") | ||
_ = mocker.patch("toml.decoder.loads", return_value=fake_config_dict) | ||
|
||
config_builder = ConfigBuilder[TestConfig]() | ||
_ = config_builder.with_root_config(TestConfig) | ||
_ = config_builder.with_configs([BazConfig]) | ||
config_type = config_builder.build() | ||
config = load_config(config_type, "foo.toml") | ||
|
||
assert hasattr(config, "baz") | ||
assert getattr(config, "baz") | ||
assert getattr(getattr(config, "baz"), "value") | ||
assert getattr(getattr(config, "baz"), "value") == "ABC" |