From 8978e463916749f224861d27063fb24afba8b954 Mon Sep 17 00:00:00 2001 From: emizrahi Date: Thu, 27 Aug 2026 18:58:17 +0300 Subject: [PATCH 1/2] perf(parser): lazy-load envelopes and BaseEnvelope to reduce cold start latency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Importing `parse` or `event_parser` from `aws_lambda_powertools.utilities.parser` previously caused all 16 envelope modules to load eagerly via the package `__init__.py`, even when no envelope was used. This added ~900ms to Lambda cold start for any function using the parser without an envelope (the common case for API Gateway handlers). The fix uses Python's module-level `__getattr__` to defer loading of `envelopes` and `BaseEnvelope` until they are actually accessed. Public API is unchanged — existing code importing from this module continues to work without modification. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../utilities/parser/__init__.py | 23 ++++++++++++++-- tests/functional/parser/test_parser.py | 27 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/parser/__init__.py b/aws_lambda_powertools/utilities/parser/__init__.py index e4e08b790b8..2003712e65b 100644 --- a/aws_lambda_powertools/utilities/parser/__init__.py +++ b/aws_lambda_powertools/utilities/parser/__init__.py @@ -1,11 +1,30 @@ """Advanced event_parser utility""" +from __future__ import annotations + +from typing import TYPE_CHECKING + from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator -from aws_lambda_powertools.utilities.parser import envelopes -from aws_lambda_powertools.utilities.parser.envelopes import BaseEnvelope from aws_lambda_powertools.utilities.parser.parser import event_parser, parse +if TYPE_CHECKING: + from aws_lambda_powertools.utilities.parser import envelopes as envelopes + from aws_lambda_powertools.utilities.parser.envelopes import BaseEnvelope + + +def __getattr__(name: str) -> object: + if name == "envelopes": + from aws_lambda_powertools.utilities.parser import envelopes as _envelopes # noqa: PLC0415 + + return _envelopes + if name == "BaseEnvelope": + from aws_lambda_powertools.utilities.parser.envelopes import BaseEnvelope as _BaseEnvelope # noqa: PLC0415 + + return _BaseEnvelope + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + __all__ = [ "event_parser", "parse", diff --git a/tests/functional/parser/test_parser.py b/tests/functional/parser/test_parser.py index 23052aa63f4..2d71402206d 100644 --- a/tests/functional/parser/test_parser.py +++ b/tests/functional/parser/test_parser.py @@ -308,3 +308,30 @@ def handler(event: SqsModel, _: LambdaContext): assert parsed_event[0].version == "version" handler(event, LambdaContext()) + + +def test_parser_import_does_not_eagerly_load_envelopes(): + """Importing parse from parser __init__ must not eagerly load all envelope modules. + + Envelopes are only needed when envelope= is passed to parse()/event_parser(). + Eager loading all 16 envelopes adds ~900ms to Lambda cold start for functions + that only use parse() without an envelope. + """ + import sys + + # Remove any previously cached parser modules to simulate a fresh import + parser_modules = [key for key in sys.modules if "aws_lambda_powertools.utilities.parser.envelopes" in key] + for mod in parser_modules: + del sys.modules[mod] + + # Also remove the parser __init__ itself so __getattr__ is exercised + sys.modules.pop("aws_lambda_powertools.utilities.parser", None) + + # Re-import — only parse is needed, envelopes should NOT be loaded + from aws_lambda_powertools.utilities.parser import parse # noqa: F401 + + envelope_modules_loaded = [key for key in sys.modules if "aws_lambda_powertools.utilities.parser.envelopes" in key] + assert not envelope_modules_loaded, ( + f"Envelope modules were eagerly loaded on parser import: {envelope_modules_loaded}. " + "This adds significant Lambda cold start latency for functions not using envelopes." + ) From 1e7f30d968476a646ace867e1ab8c8dcc260f3be Mon Sep 17 00:00:00 2001 From: emizrahi Date: Mon, 7 Sep 2026 09:55:07 +0300 Subject: [PATCH 2/2] fix(parser): address review feedback - use importlib and subprocess tests - Fix RecursionError by using importlib.import_module() and caching in globals() - Improve test isolation by running tests in fresh subprocesses - Add tests for all public import paths (parse, envelopes, BaseEnvelope, import *) - Ensure backward compatibility for all existing public APIs Co-Authored-By: Claude Sonnet 4.5 --- .../utilities/parser/__init__.py | 12 +-- tests/functional/parser/test_parser.py | 81 +++++++++++++++---- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/aws_lambda_powertools/utilities/parser/__init__.py b/aws_lambda_powertools/utilities/parser/__init__.py index 2003712e65b..975a4610f9d 100644 --- a/aws_lambda_powertools/utilities/parser/__init__.py +++ b/aws_lambda_powertools/utilities/parser/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +import importlib from typing import TYPE_CHECKING from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator @@ -15,13 +16,14 @@ def __getattr__(name: str) -> object: if name == "envelopes": - from aws_lambda_powertools.utilities.parser import envelopes as _envelopes # noqa: PLC0415 - + _envelopes = importlib.import_module(f"{__name__}.envelopes") + globals()[name] = _envelopes return _envelopes if name == "BaseEnvelope": - from aws_lambda_powertools.utilities.parser.envelopes import BaseEnvelope as _BaseEnvelope # noqa: PLC0415 - - return _BaseEnvelope + _envelopes_module = importlib.import_module(f"{__name__}.envelopes") + _base_envelope = _envelopes_module.BaseEnvelope + globals()[name] = _base_envelope + return _base_envelope raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tests/functional/parser/test_parser.py b/tests/functional/parser/test_parser.py index 2d71402206d..f824c473e72 100644 --- a/tests/functional/parser/test_parser.py +++ b/tests/functional/parser/test_parser.py @@ -316,22 +316,73 @@ def test_parser_import_does_not_eagerly_load_envelopes(): Envelopes are only needed when envelope= is passed to parse()/event_parser(). Eager loading all 16 envelopes adds ~900ms to Lambda cold start for functions that only use parse() without an envelope. + + This test runs in a fresh subprocess to ensure reliable isolation from any + modules already loaded by conftest or other tests. """ + import subprocess import sys - # Remove any previously cached parser modules to simulate a fresh import - parser_modules = [key for key in sys.modules if "aws_lambda_powertools.utilities.parser.envelopes" in key] - for mod in parser_modules: - del sys.modules[mod] - - # Also remove the parser __init__ itself so __getattr__ is exercised - sys.modules.pop("aws_lambda_powertools.utilities.parser", None) - - # Re-import — only parse is needed, envelopes should NOT be loaded - from aws_lambda_powertools.utilities.parser import parse # noqa: F401 - - envelope_modules_loaded = [key for key in sys.modules if "aws_lambda_powertools.utilities.parser.envelopes" in key] - assert not envelope_modules_loaded, ( - f"Envelope modules were eagerly loaded on parser import: {envelope_modules_loaded}. " - "This adds significant Lambda cold start latency for functions not using envelopes." + # Test 1: importing parse alone should NOT load envelopes + script_parse_only = """ +import sys +from aws_lambda_powertools.utilities.parser import parse +envelope_modules = [key for key in sys.modules if "aws_lambda_powertools.utilities.parser.envelopes" in key] +assert not envelope_modules, f"Envelope modules loaded on parse import: {envelope_modules}" +print("PASS: parse import does not load envelopes") +""" + + result = subprocess.run([sys.executable, "-c", script_parse_only], capture_output=True, text=True, check=False) + assert result.returncode == 0, f"parse import test failed:\n{result.stderr}\n{result.stdout}" + + # Test 2: importing envelopes explicitly SHOULD load envelopes + script_envelopes_import = """ +import sys +from aws_lambda_powertools.utilities.parser import envelopes +assert "aws_lambda_powertools.utilities.parser.envelopes" in sys.modules, "envelopes module not loaded" +assert hasattr(envelopes, "SqsEnvelope"), "envelopes.SqsEnvelope not accessible" +print("PASS: envelopes import works correctly") +""" + + result = subprocess.run( + [sys.executable, "-c", script_envelopes_import], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, f"envelopes import test failed:\n{result.stderr}\n{result.stdout}" + + # Test 3: importing BaseEnvelope explicitly SHOULD load envelopes + script_base_envelope_import = """ +import sys +from aws_lambda_powertools.utilities.parser import BaseEnvelope +assert "aws_lambda_powertools.utilities.parser.envelopes" in sys.modules, "envelopes module not loaded" +assert BaseEnvelope.__name__ == "BaseEnvelope", "BaseEnvelope not correctly imported" +print("PASS: BaseEnvelope import works correctly") +""" + + result = subprocess.run( + [sys.executable, "-c", script_base_envelope_import], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, f"BaseEnvelope import test failed:\n{result.stderr}\n{result.stdout}" + + # Test 4: from parser import * should work and load all public exports + script_import_star = """ +from aws_lambda_powertools.utilities.parser import * +assert 'parse' in dir(), "parse not available after import *" +assert 'event_parser' in dir(), "event_parser not available after import *" +assert 'envelopes' in dir(), "envelopes not available after import *" +assert 'BaseEnvelope' in dir(), "BaseEnvelope not available after import *" +print("PASS: import * works correctly") +""" + + result = subprocess.run( + [sys.executable, "-c", script_import_star], + capture_output=True, + text=True, + check=False, ) + assert result.returncode == 0, f"import * test failed:\n{result.stderr}\n{result.stdout}"