From 7aa28e5b851d45851c7ea3e43251eee4b2c2bfc0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 17 Oct 2026 15:00:10 +0200 Subject: [PATCH] Make imports lazy on Python 3.15 --- CHANGELOG.md | 5 +++++ src/structlog/__init__.py | 12 ++++++++++++ src/structlog/_config.py | 3 +++ src/structlog/_frames.py | 3 +++ src/structlog/_native.py | 3 +++ src/structlog/_output.py | 3 +++ src/structlog/_utils.py | 3 +++ src/structlog/dev.py | 3 +++ src/structlog/processors.py | 15 +++++++++------ src/structlog/stdlib.py | 3 +++ src/structlog/threadlocal.py | 3 +++ src/structlog/tracebacks.py | 3 +++ tests/processors/test_renderers.py | 14 +++++++------- tests/test_packaging.py | 29 +++++++++++++++++++++++++++++ 14 files changed, 89 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b23d71a7..e2bf1b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/ ## [Unreleased](https://github.com/hynek/structlog/compare/26.1.0...HEAD) +### Changed + +- On Python 3.15 and later, *structlog* uses [PEP 810](https://peps.python.org/pep-0810/) lazy imports for modules that are only needed once you log. + This makes `import structlog` significantly faster. + ## [26.1.0](https://github.com/hynek/structlog/compare/25.5.0...26.1.0) - 2026-06-06 diff --git a/src/structlog/__init__.py b/src/structlog/__init__.py index 0c72e360..d70cd15a 100644 --- a/src/structlog/__init__.py +++ b/src/structlog/__init__.py @@ -6,6 +6,18 @@ from __future__ import annotations + +__lazy_modules__ = [ + "structlog", + "structlog._base", + "structlog._config", + "structlog._generic", + "structlog._native", + "structlog._output", + "structlog.exceptions", + "structlog.testing", +] + from structlog import ( contextvars, dev, diff --git a/src/structlog/_config.py b/src/structlog/_config.py index 22fa589f..c7233624 100644 --- a/src/structlog/_config.py +++ b/src/structlog/_config.py @@ -9,6 +9,9 @@ from __future__ import annotations + +__lazy_modules__ = ["warnings"] + import os import sys import warnings diff --git a/src/structlog/_frames.py b/src/structlog/_frames.py index dbd15d9c..1591ebed 100644 --- a/src/structlog/_frames.py +++ b/src/structlog/_frames.py @@ -5,6 +5,9 @@ from __future__ import annotations + +__lazy_modules__ = ["traceback"] + import sys import traceback diff --git a/src/structlog/_native.py b/src/structlog/_native.py index 5775b8de..5062d360 100644 --- a/src/structlog/_native.py +++ b/src/structlog/_native.py @@ -9,6 +9,9 @@ from __future__ import annotations + +__lazy_modules__ = ["asyncio", "threading"] + import asyncio import collections import contextvars diff --git a/src/structlog/_output.py b/src/structlog/_output.py index 68fa976b..dd9c1f6c 100644 --- a/src/structlog/_output.py +++ b/src/structlog/_output.py @@ -9,6 +9,9 @@ from __future__ import annotations + +__lazy_modules__ = ["copy", "pickle", "threading"] + import copy import sys import threading diff --git a/src/structlog/_utils.py b/src/structlog/_utils.py index 9a41c257..031f74b5 100644 --- a/src/structlog/_utils.py +++ b/src/structlog/_utils.py @@ -9,6 +9,9 @@ from __future__ import annotations + +__lazy_modules__ = ["contextlib"] + import sys from contextlib import suppress diff --git a/src/structlog/dev.py b/src/structlog/dev.py index 8ab25d15..85b72029 100644 --- a/src/structlog/dev.py +++ b/src/structlog/dev.py @@ -11,6 +11,9 @@ from __future__ import annotations + +__lazy_modules__ = ["warnings"] + import sys import warnings diff --git a/src/structlog/processors.py b/src/structlog/processors.py index 8eefaecc..a59e0a1b 100644 --- a/src/structlog/processors.py +++ b/src/structlog/processors.py @@ -9,7 +9,10 @@ from __future__ import annotations -import datetime + +__lazy_modules__ = ["datetime", "logging", "threading"] + +import datetime as dt import enum import json import logging @@ -523,19 +526,19 @@ def _make_stamper( msg = "UNIX timestamps are always UTC." raise ValueError(msg) - now: Callable[[], datetime.datetime] + now: Callable[[], dt.datetime] if utc: - def now() -> datetime.datetime: - return datetime.datetime.now(tz=datetime.timezone.utc) + def now() -> dt.datetime: + return dt.datetime.now(tz=dt.timezone.utc) else: - def now() -> datetime.datetime: + def now() -> dt.datetime: # We don't need the TZ for our own formatting. We add it only for # user-defined formats later. - return datetime.datetime.now() # noqa: DTZ005 + return dt.datetime.now() # noqa: DTZ005 if fmt is None: diff --git a/src/structlog/stdlib.py b/src/structlog/stdlib.py index 2906598d..ae2df984 100644 --- a/src/structlog/stdlib.py +++ b/src/structlog/stdlib.py @@ -11,6 +11,9 @@ from __future__ import annotations + +__lazy_modules__ = ["asyncio", "threading", "warnings"] + import asyncio import contextvars import functools diff --git a/src/structlog/threadlocal.py b/src/structlog/threadlocal.py index f8dba786..ead5d1f5 100644 --- a/src/structlog/threadlocal.py +++ b/src/structlog/threadlocal.py @@ -14,6 +14,9 @@ from __future__ import annotations + +__lazy_modules__ = ["uuid", "warnings"] + import contextlib import sys import threading diff --git a/src/structlog/tracebacks.py b/src/structlog/tracebacks.py index 5b01ad99..99fe574e 100644 --- a/src/structlog/tracebacks.py +++ b/src/structlog/tracebacks.py @@ -14,6 +14,9 @@ from __future__ import annotations + +__lazy_modules__ = ["traceback"] + import os import os.path import sys diff --git a/tests/processors/test_renderers.py b/tests/processors/test_renderers.py index 82645633..77c49cfd 100644 --- a/tests/processors/test_renderers.py +++ b/tests/processors/test_renderers.py @@ -5,7 +5,7 @@ from __future__ import annotations -import datetime +import datetime as dt import json import pickle @@ -331,7 +331,7 @@ def test_FallbackEncoder_falls_back(self): The fallback handler uses repr if it doesn't know the type. """ s = json.dumps( - {"date": datetime.date(1980, 3, 25)}, + {"date": dt.date(1980, 3, 25)}, default=_json_fallback_handler, ) @@ -351,7 +351,7 @@ def test_custom_fallback(self): A custom fallback handler can be used. """ jr = JSONRenderer(default=lambda x: repr(x)[::-1]) - d = {"date": datetime.date(1980, 3, 25)} + d = {"date": dt.date(1980, 3, 25)} assert '{"date": ")52 ,3 ,0891(etad.emitetad"}' == jr(None, None, d) @@ -411,7 +411,7 @@ def test_formats(self): assert "1980" == d["timestamp"] @time_machine.travel( - datetime.datetime(1980, 3, 25, 16, 0, 0, tzinfo=datetime.timezone.utc) + dt.datetime(1980, 3, 25, 16, 0, 0, tzinfo=dt.timezone.utc) ) def test_inserts_formatted_utc(self): """ @@ -428,7 +428,7 @@ def test_inserts_formatted_local(self): """ The fmt string in local timezone works. """ - local_tz = datetime.datetime.now().astimezone().tzname() + local_tz = dt.datetime.now().astimezone().tzname() ts = TimeStamper(fmt="%Y-%m-%d %H:%M:%S %Z", utc=False) d = ts(None, None, {}) @@ -442,11 +442,11 @@ def test_tz_aware(self): ts = TimeStamper(fmt="%z") d = ts(None, None, {}) - assert "" == datetime.datetime.now().strftime("%z") # noqa: DTZ005 + assert "" == dt.datetime.now().strftime("%z") # noqa: DTZ005 assert "" != d["timestamp"] @time_machine.travel( - datetime.datetime(1980, 3, 25, 16, 0, 0, tzinfo=datetime.timezone.utc) + dt.datetime(1980, 3, 25, 16, 0, 0, tzinfo=dt.timezone.utc) ) def test_adds_Z_to_iso(self): """ diff --git a/tests/test_packaging.py b/tests/test_packaging.py index d8c470a0..6c85184f 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -3,6 +3,10 @@ # 2.0, and the MIT License. See the LICENSE file in the root of this # repository for complete details. +import json +import subprocess +import sys + from importlib import metadata import pytest @@ -47,3 +51,28 @@ def test_does_not_exist(self): AttributeError, match="module structlog has no attribute __yolo__" ): structlog.__yolo__ + + +@pytest.mark.skipif( + sys.version_info < (3, 15), reason="PEP 810 lazy imports need Python 3.15+" +) +class TestLazyImports: + def test_import_does_not_load_asyncio(self): + """ + Importing structlog in a fresh interpreter does not import asyncio, + because modules that are only needed when logging are imported lazily. + """ + script = ( + "import json, sys\n" + "before = set(sys.modules)\n" + "import structlog\n" + "print(json.dumps(sorted(set(sys.modules) - before)))\n" + ) + out = subprocess.run( + [sys.executable, "-c", script], + check=True, + capture_output=True, + text=True, + ).stdout + + assert "asyncio" not in json.loads(out)