From 852196897a77449579da1caea1390bcb4ffbf202 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 3 Oct 2023 13:47:49 -0700 Subject: [PATCH] Allow log level enum with any case When configuring a FastAPI application with debug logging, I used "debug" instead of "DEBUG" in the configuration and was then annoyed that the server didn't start with an error. The function call supported a string with any case, but the Pydantic model we use to validate most configurations required uppercase because it relied on the enum to do the validation. Implement a _missing_ method for the LogLevel enum that converts strings in any case to the expected uppercase, which allows Pydantic validation of models to work with any case of the log level and still validates and returns the correct log level. --- changelog.d/20231003_135049_rra_DM_23878.md | 3 +++ src/safir/logging.py | 19 +++++++++++++++++-- tests/logging_test.py | 20 ++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 changelog.d/20231003_135049_rra_DM_23878.md 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)