diff --git a/aws_lambda_powertools/utilities/parser/__init__.py b/aws_lambda_powertools/utilities/parser/__init__.py index e4e08b790b8..975a4610f9d 100644 --- a/aws_lambda_powertools/utilities/parser/__init__.py +++ b/aws_lambda_powertools/utilities/parser/__init__.py @@ -1,11 +1,32 @@ """Advanced event_parser utility""" +from __future__ import annotations + +import importlib +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": + _envelopes = importlib.import_module(f"{__name__}.envelopes") + globals()[name] = _envelopes + return _envelopes + if name == "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}") + + __all__ = [ "event_parser", "parse", diff --git a/tests/functional/parser/test_parser.py b/tests/functional/parser/test_parser.py index 23052aa63f4..f824c473e72 100644 --- a/tests/functional/parser/test_parser.py +++ b/tests/functional/parser/test_parser.py @@ -308,3 +308,81 @@ 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. + + 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 + + # 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}"