diff --git a/changelog.d/20231003_135049_rra_DM_23878.md b/changelog.d/20231003_135049_rra_DM_23878.md new file mode 100644 index 00000000..d9ca374a --- /dev/null +++ b/changelog.d/20231003_135049_rra_DM_23878.md @@ -0,0 +1,3 @@ +### New features + +- Allow the `safir.logging.LogLevel` enum to be created from strings of any case, which will allow the logging level to be specified with any case for Safir applications that use Pydantic to validate the field. diff --git a/src/safir/logging.py b/src/safir/logging.py index 272357f1..13f8bd96 100644 --- a/src/safir/logging.py +++ b/src/safir/logging.py @@ -7,7 +7,7 @@ import re import sys from enum import Enum -from typing import Any +from typing import Any, Self import structlog from structlog.stdlib import add_log_level @@ -48,7 +48,11 @@ class Profile(Enum): class LogLevel(Enum): - """Python logging level.""" + """Python logging level. + + Any case variation is accepted when converting a string to an enum value + via the class constructor. + """ DEBUG = "DEBUG" INFO = "INFO" @@ -56,6 +60,17 @@ class LogLevel(Enum): ERROR = "ERROR" CRITICAL = "CRITICAL" + @classmethod + def _missing_(cls, value: Any) -> Self | None: + """Allow strings in any case to be used to create the enum.""" + if not isinstance(value, str): + return None + value = value.upper() + for member in cls: + if member.value == value: + return member + return None + def add_log_severity( logger: logging.Logger, method_name: str, event_dict: EventDict diff --git a/tests/logging_test.py b/tests/logging_test.py index eaef02c8..03fd0e83 100644 --- a/tests/logging_test.py +++ b/tests/logging_test.py @@ -14,6 +14,7 @@ from _pytest.capture import CaptureFixture from _pytest.logging import LogCaptureFixture from httpx import AsyncClient +from pydantic import BaseModel, ValidationError from safir import logging as safir_logging from safir.logging import LogLevel, Profile, configure_logging @@ -48,6 +49,25 @@ def _strip_color(string: str) -> str: return re.sub(r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]", "", string) +def test_log_level_enum() -> None: + """Test that any case is allowed when initializing the enum.""" + assert LogLevel("warning") == LogLevel.WARNING + assert LogLevel("WARNING") == LogLevel.WARNING + assert LogLevel("Error") == LogLevel.ERROR + + # Check that this also works when going through Pydantic. + class Model(BaseModel): + log_level: LogLevel + + model = Model.model_validate({"log_level": "warning"}) + assert model.log_level == LogLevel.WARNING + model = Model.model_validate({"log_level": "inFO"}) + assert model.log_level == LogLevel.INFO + + with pytest.raises(ValidationError): + Model.model_validate({"log_level": "unknown"}) + + def test_configure_logging_development(caplog: LogCaptureFixture) -> None: """Test that development-mode logging is key-value formatted.""" caplog.set_level(logging.INFO)