diff --git a/aws_lambda_powertools/utilities/parser/__init__.py b/aws_lambda_powertools/utilities/parser/__init__.py index e4e08b790b8..7870a2be74a 100644 --- a/aws_lambda_powertools/utilities/parser/__init__.py +++ b/aws_lambda_powertools/utilities/parser/__init__.py @@ -1,11 +1,36 @@ """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}") + + +def __dir__() -> list[str]: + return sorted(set(globals()) | set(__all__)) + + __all__ = [ "event_parser", "parse", diff --git a/tests/functional/parser/_parser_import_probe.py b/tests/functional/parser/_parser_import_probe.py new file mode 100644 index 00000000000..9747b5f8b60 --- /dev/null +++ b/tests/functional/parser/_parser_import_probe.py @@ -0,0 +1,31 @@ +import importlib +import inspect +import sys + +ENVELOPE_MODULE = "aws_lambda_powertools.utilities.parser.envelopes" +scenario = sys.argv[1] + +if scenario == "lazy": + parser = importlib.import_module("aws_lambda_powertools.utilities.parser") + + assert not any(module == ENVELOPE_MODULE or module.startswith(f"{ENVELOPE_MODULE}.") for module in sys.modules) + assert {"envelopes", "BaseEnvelope"} <= set(dir(parser)) + assert ENVELOPE_MODULE not in sys.modules + + members = dict(inspect.getmembers(parser)) + assert members["envelopes"] is parser.envelopes + assert members["BaseEnvelope"] is parser.BaseEnvelope + assert ENVELOPE_MODULE in sys.modules +elif scenario == "star": + from aws_lambda_powertools.utilities.parser import * # noqa: E402,F403 + + expected = { + "parse", + "event_parser", + "envelopes", + "BaseEnvelope", + } + assert expected <= globals().keys() + assert ENVELOPE_MODULE in sys.modules +else: + raise ValueError(f"Unknown scenario: {scenario}") diff --git a/tests/functional/parser/test_parser.py b/tests/functional/parser/test_parser.py index 23052aa63f4..0a575abb932 100644 --- a/tests/functional/parser/test_parser.py +++ b/tests/functional/parser/test_parser.py @@ -1,5 +1,9 @@ import json +import os +import subprocess +import sys from datetime import datetime +from pathlib import Path from typing import Any, Dict, Literal, Union import pydantic @@ -308,3 +312,22 @@ def handler(event: SqsModel, _: LambdaContext): assert parsed_event[0].version == "version" handler(event, LambdaContext()) + + +@pytest.mark.parametrize("scenario", ["lazy", "star"]) +def test_parser_imports_in_clean_interpreter(scenario): + project_root = Path(__file__).parents[3] + probe = Path(__file__).with_name("_parser_import_probe.py") + env = os.environ.copy() + env["PYTHONPATH"] = str(project_root) + + result = subprocess.run( + [sys.executable, str(probe), scenario], + cwd=project_root, + env=env, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr