diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c0099..6396746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ All notable changes to this project will be documented in this file. * New function `is_valid_syntax_pct_encoded` (via [#48]) * New dict `RFC3987_SYNTAX_TERM_VALIDATORS` mapping `RFC3987_SYNTAX_TERMS` to validator function (via [#49]) * Performance - * Public symbols use lazy loading (via [#43]) + * Public symbols use lazy loading (via [#43], [#59]) * Tests * Add grammar regression tests (via [#48]) @@ -36,6 +36,7 @@ All notable changes to this project will be documented in this file. [#50]: https://github.com/jkowalleck/rfc3987-syntax2/pull/50 [#51]: https://github.com/jkowalleck/rfc3987-syntax2/pull/51 [#52]: https://github.com/jkowalleck/rfc3987-syntax2/pull/52 +[#59]: https://github.com/jkowalleck/rfc3987-syntax2/pull/59 ## v1.2.0 - 2026-07-16 diff --git a/src/rfc3987_syntax2/__init__.py b/src/rfc3987_syntax2/__init__.py index 7300822..3de7209 100644 --- a/src/rfc3987_syntax2/__init__.py +++ b/src/rfc3987_syntax2/__init__.py @@ -2,4 +2,22 @@ # Copyright (c) 2026 Jan Kowalleck - modifications and maintenance # SPDX-License-Identifier: MIT -from .syntax_helpers import * +from typing import Any + +from . import syntax_helpers as _syntax_helpers + +__all__ = _syntax_helpers.__all__ + +_EXPORTS_SH = frozenset(_syntax_helpers.__all__) + + +def __getattr__(name: str) -> Any: + if name in _EXPORTS_SH: + value = getattr(_syntax_helpers, name) + globals()[name] = value + return value + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__() -> list[str]: + return sorted(set(globals().keys()) | set(__all__)) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index fd1ebda..9b82840 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -11,6 +11,7 @@ from .utils import load_grammar + __all__ = [ "RFC3987_SYNTAX_PARSER_TYPE", "RFC3987_SYNTAX_GRAMMAR_PATH", @@ -64,7 +65,6 @@ "is_valid_syntax_pct_encoded", ] - RFC3987_SYNTAX_PARSER_TYPE: str = "earley" RFC3987_SYNTAX_GRAMMAR_PATH: Path = Path(__file__).parent / "syntax_rfc3987.lark" RFC3987_SYNTAX_TERMS: list[str] = [ @@ -320,14 +320,15 @@ def _get_syntax_parser() -> Lark: def __getattr__(name: str) -> Any: - if name not in __all__: - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - if name == 'grammar': - return _get_grammar() - if name == 'syntax_parser': - return _get_syntax_parser() - raise AttributeError(f"module {__name__!r} failed to implement attribute {name!r}") - + if name == "grammar": + global grammar + grammar = _get_grammar() + return grammar + if name == "syntax_parser": + global syntax_parser + syntax_parser = _get_syntax_parser() + return syntax_parser + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") def __dir__() -> list[str]: return sorted(set(globals().keys()) | set(__all__)) diff --git a/tests/test_lazy_attrs.py b/tests/test_lazy_attrs.py index 2267ca2..29266a4 100644 --- a/tests/test_lazy_attrs.py +++ b/tests/test_lazy_attrs.py @@ -1,10 +1,33 @@ # Copyright (c) 2026 Jan Kowalleck - modifications and maintenance # SPDX-License-Identifier: MIT +import subprocess +import sys + import rfc3987_syntax2 as sut from lark import Lark +def test_grammar_not_eager() -> None: + """Regression test: grammar is not populated in rfc3987_syntax2.__dict__ immediately after import.""" + result = subprocess.run( + [sys.executable, '-c', 'import rfc3987_syntax2; assert "grammar" not in vars(rfc3987_syntax2), "grammar was eagerly loaded on import"'], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + + +def test_syntax_parser_not_eager() -> None: + """Regression test: syntax_parser is not populated in rfc3987_syntax2.__dict__ immediately after import.""" + result = subprocess.run( + [sys.executable, '-c', 'import rfc3987_syntax2; assert "syntax_parser" not in vars(rfc3987_syntax2), "syntax_parser was eagerly loaded on import"'], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + + def test_grammar_is_accessible() -> None: """Regression test: grammar is lazily loaded and accessible as a module attribute.""" assert isinstance(sut.grammar, str)