Skip to content

Commit

Permalink
Allow log level enum with any case
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rra committed Oct 3, 2023
1 parent 15ed2c0 commit 8521968
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 2 deletions.
3 changes: 3 additions & 0 deletions changelog.d/20231003_135049_rra_DM_23878.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 17 additions & 2 deletions src/safir/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,14 +48,29 @@ 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"
WARNING = "WARNING"
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
Expand Down
20 changes: 20 additions & 0 deletions tests/logging_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 8521968

Please sign in to comment.