Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -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

Expand Down
20 changes: 19 additions & 1 deletion src/rfc3987_syntax2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Comment thread
jkowalleck marked this conversation as resolved.

def __getattr__(name: str) -> Any:
if name in _EXPORTS_SH:
value = getattr(_syntax_helpers, name)
globals()[name] = value
return value
Comment thread
jkowalleck marked this conversation as resolved.
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Comment thread
jkowalleck marked this conversation as resolved.


def __dir__() -> list[str]:
return sorted(set(globals().keys()) | set(__all__))
19 changes: 10 additions & 9 deletions src/rfc3987_syntax2/syntax_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from .utils import load_grammar


__all__ = [
"RFC3987_SYNTAX_PARSER_TYPE",
"RFC3987_SYNTAX_GRAMMAR_PATH",
Expand Down Expand Up @@ -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] = [
Expand Down Expand Up @@ -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__))
Expand Down
23 changes: 23 additions & 0 deletions tests/test_lazy_attrs.py
Original file line number Diff line number Diff line change
@@ -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
Comment thread
jkowalleck marked this conversation as resolved.


def test_grammar_is_accessible() -> None:
"""Regression test: grammar is lazily loaded and accessible as a module attribute."""
assert isinstance(sut.grammar, str)
Expand Down