diff --git a/ecs_logging/_stdlib.py b/ecs_logging/_stdlib.py index 79843f6..8f8f9b0 100644 --- a/ecs_logging/_stdlib.py +++ b/ecs_logging/_stdlib.py @@ -32,10 +32,10 @@ from typing import Any, Callable, Dict, Optional, Sequence, Union -try: - from typing import Literal # type: ignore -except ImportError: - from typing_extensions import Literal # type: ignore +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal # Load the attributes of a LogRecord so if some are @@ -108,13 +108,20 @@ def __init__( exclude_keys=["error"] """ - _kwargs = {} - if validate is not None: - # validate was introduced in py3.8 so we need to only provide it if the user provided it - _kwargs["validate"] = validate - super().__init__( # type: ignore[call-arg] - fmt=fmt, datefmt=datefmt, style=style, **_kwargs # type: ignore[arg-type] - ) + # validate was introduced in py3.8 so we need to only provide it if the user provided it + if sys.version_info >= (3, 8) and validate is not None: + super().__init__( + fmt=fmt, + datefmt=datefmt, + style=style, + validate=validate, + ) + else: + super().__init__( + fmt=fmt, + datefmt=datefmt, + style=style, + ) if stack_trace_limit is not None: if not isinstance(stack_trace_limit, int): diff --git a/ecs_logging/_structlog.py b/ecs_logging/_structlog.py index d60af75..6cb93c3 100644 --- a/ecs_logging/_structlog.py +++ b/ecs_logging/_structlog.py @@ -17,7 +17,13 @@ import time import datetime -from typing import Any, Dict +import sys +from typing import Any + +if sys.version_info >= (3, 9): + from collections.abc import MutableMapping +else: + from typing import MutableMapping from ._meta import ECS_VERSION from ._utils import json_dumps, normalize_dict @@ -32,7 +38,7 @@ def __init__( ) -> None: self.ensure_ascii = ensure_ascii - def __call__(self, _: Any, name: str, event_dict: Dict[str, Any]) -> str: + def __call__(self, _: Any, name: str, event_dict: MutableMapping[str, Any]) -> str: # Handle event -> message now so that stuff like `event.dataset` doesn't # cause problems down the line @@ -42,7 +48,9 @@ def __call__(self, _: Any, name: str, event_dict: Dict[str, Any]) -> str: event_dict = self.format_to_ecs(event_dict) return self._json_dumps(event_dict) - def format_to_ecs(self, event_dict: Dict[str, Any]) -> Dict[str, Any]: + def format_to_ecs( + self, event_dict: MutableMapping[str, Any] + ) -> MutableMapping[str, Any]: if "@timestamp" not in event_dict: event_dict["@timestamp"] = ( datetime.datetime.fromtimestamp( @@ -61,5 +69,5 @@ def format_to_ecs(self, event_dict: Dict[str, Any]) -> Dict[str, Any]: event_dict.setdefault("ecs.version", ECS_VERSION) return event_dict - def _json_dumps(self, value: Dict[str, Any]) -> str: + def _json_dumps(self, value: MutableMapping[str, Any]) -> str: return json_dumps(value=value, ensure_ascii=self.ensure_ascii) diff --git a/ecs_logging/_utils.py b/ecs_logging/_utils.py index c23a730..311fefa 100644 --- a/ecs_logging/_utils.py +++ b/ecs_logging/_utils.py @@ -18,8 +18,15 @@ import collections.abc import json import functools +import sys from typing import Any, Dict, Mapping +if sys.version_info >= (3, 9): + from collections.abc import MutableMapping +else: + from typing import MutableMapping + + __all__ = [ "normalize_dict", "de_dot", @@ -52,9 +59,9 @@ def flatten_dict(value: Mapping[str, Any]) -> Dict[str, Any]: return top_level -def normalize_dict(value: Dict[str, Any]) -> Dict[str, Any]: +def normalize_dict(value: MutableMapping[str, Any]) -> MutableMapping[str, Any]: """Expands all dotted names to nested dictionaries""" - if not isinstance(value, dict): + if not isinstance(value, MutableMapping): return value keys = list(value.keys()) for key in keys: @@ -77,7 +84,9 @@ def de_dot(dot_string: str, msg: Any) -> Dict[str, Any]: return ret -def merge_dicts(from_: Dict[Any, Any], into: Dict[Any, Any]) -> Dict[Any, Any]: +def merge_dicts( + from_: Mapping[Any, Any], into: MutableMapping[Any, Any] +) -> MutableMapping[Any, Any]: """Merge deeply nested dictionary structures. When called has side-effects within 'destination'. """ @@ -97,7 +106,7 @@ def merge_dicts(from_: Dict[Any, Any], into: Dict[Any, Any]) -> Dict[Any, Any]: return into -def json_dumps(value: Dict[str, Any], ensure_ascii: bool = True) -> str: +def json_dumps(value: MutableMapping[str, Any], ensure_ascii: bool = True) -> str: # Ensure that the first three fields are '@timestamp', # 'log.level', and 'message' per ECS spec diff --git a/noxfile.py b/noxfile.py index 6e8572b..6467272 100644 --- a/noxfile.py +++ b/noxfile.py @@ -57,6 +57,5 @@ def lint(session): "mypy", "--strict", "--show-error-codes", - "--no-warn-unused-ignores", "ecs_logging/", ) diff --git a/pyproject.toml b/pyproject.toml index 9199344..54e0cb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ develop = [ "mock", "structlog", "elastic-apm", + "mypy", ] [tool.flit.metadata.urls] diff --git a/tests/test_typing.py b/tests/test_typing.py new file mode 100644 index 0000000..f804742 --- /dev/null +++ b/tests/test_typing.py @@ -0,0 +1,18 @@ +import pathlib +import subprocess +import sys + +import pytest + +_THIS_DIR = pathlib.Path(__file__).parent + + +@pytest.mark.parametrize("file", list((_THIS_DIR / "typing").glob("*.py"))) +@pytest.mark.xfail( + sys.version_info < (3, 10), + reason="Older versions of mypy hit https://github.com/python/mypy/issues/16947 on StdlibFormatter.converter", +) +def test_type_check(file): + subprocess.check_call( + [sys.executable, "-m", "mypy", "--strict", "--config-file=", file] + ) diff --git a/tests/typing/structlog_usage.py b/tests/typing/structlog_usage.py new file mode 100644 index 0000000..8dbf61f --- /dev/null +++ b/tests/typing/structlog_usage.py @@ -0,0 +1,33 @@ +import sys +from typing import List, Tuple + +import ecs_logging +import structlog +from structlog.typing import Processor + +shared_processors: Tuple[Processor, ...] = ( + structlog.contextvars.merge_contextvars, + structlog.processors.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.set_exc_info, + structlog.processors.TimeStamper(fmt="iso", utc=True), +) + +processors: List[Processor] +if sys.stderr.isatty(): + processors = [ + *shared_processors, + structlog.dev.ConsoleRenderer(), + ] +else: + processors = [ + *shared_processors, + structlog.processors.dict_tracebacks, + ecs_logging.StructlogFormatter(), + ] + +structlog.configure( + processors=processors, + logger_factory=structlog.PrintLoggerFactory(), + cache_logger_on_first_use=True, +)