From 340d40e40fe3ce95fbc10c2b4660b449c6ca1406 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 21 Jul 2026 15:08:03 +0200 Subject: [PATCH 01/11] perf: lazy loading Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/__init__.py | 67 ++++++++++++++++++++++++++- src/rfc3987_syntax2/syntax_helpers.py | 56 +--------------------- 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/src/rfc3987_syntax2/__init__.py b/src/rfc3987_syntax2/__init__.py index 7300822..a0e77f8 100644 --- a/src/rfc3987_syntax2/__init__.py +++ b/src/rfc3987_syntax2/__init__.py @@ -2,4 +2,69 @@ # 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__ = [ + "RFC3987_SYNTAX_PARSER_TYPE", + "RFC3987_SYNTAX_GRAMMAR_PATH", + "RFC3987_SYNTAX_TERMS", + "grammar", + "syntax_parser", + "parse", + "is_valid_syntax", + "make_syntax_validator", + "RFC3987_SYNTAX_TERM_VALIDATORS", + "is_valid_syntax_iri", + "is_valid_syntax_iri_reference", + "is_valid_syntax_absolute_iri", + "is_valid_syntax_scheme", + "is_valid_syntax_ihier_part", + "is_valid_syntax_irelative_ref", + "is_valid_syntax_irelative_part", + "is_valid_syntax_iauthority", + "is_valid_syntax_iuserinfo", + "is_valid_syntax_ihost", + "is_valid_syntax_ireg_name", + "is_valid_syntax_ipath", + "is_valid_syntax_ipath_abempty", + "is_valid_syntax_ipath_absolute", + "is_valid_syntax_ipath_noscheme", + "is_valid_syntax_ipath_rootless", + "is_valid_syntax_ipath_empty", + "is_valid_syntax_isegment", + "is_valid_syntax_isegment_nz", + "is_valid_syntax_isegment_nz_nc", + "is_valid_syntax_ipchar", + "is_valid_syntax_iquery", + "is_valid_syntax_ifragment", + "is_valid_syntax_iunreserved", + "is_valid_syntax_ucschar", + "is_valid_syntax_iprivate", + "is_valid_syntax_sub_delims", + "is_valid_syntax_ip_literal", + "is_valid_syntax_ipvfuture", + "is_valid_syntax_ipv6address", + "is_valid_syntax_h16", + "is_valid_syntax_ls32", + "is_valid_syntax_ipv4address", + "is_valid_syntax_dec_octet", + "is_valid_syntax_digit", + "is_valid_syntax_non_zero", + "is_valid_syntax_unreserved", + "is_valid_syntax_alpha", + "is_valid_syntax_hexdig", + "is_valid_syntax_port", + "is_valid_syntax_pct_encoded", +] + + +def __getattr__(name: str) -> Any: + if name in __all__: + return getattr(_syntax_helpers, name) + 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..f18eebd 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -11,58 +11,6 @@ from .utils import load_grammar -__all__ = [ - "RFC3987_SYNTAX_PARSER_TYPE", - "RFC3987_SYNTAX_GRAMMAR_PATH", - "RFC3987_SYNTAX_TERMS", - "grammar", - "syntax_parser", - "parse", - "is_valid_syntax", - "make_syntax_validator", - "RFC3987_SYNTAX_TERM_VALIDATORS", - "is_valid_syntax_iri", - "is_valid_syntax_iri_reference", - "is_valid_syntax_absolute_iri", - "is_valid_syntax_scheme", - "is_valid_syntax_ihier_part", - "is_valid_syntax_irelative_ref", - "is_valid_syntax_irelative_part", - "is_valid_syntax_iauthority", - "is_valid_syntax_iuserinfo", - "is_valid_syntax_ihost", - "is_valid_syntax_ireg_name", - "is_valid_syntax_ipath", - "is_valid_syntax_ipath_abempty", - "is_valid_syntax_ipath_absolute", - "is_valid_syntax_ipath_noscheme", - "is_valid_syntax_ipath_rootless", - "is_valid_syntax_ipath_empty", - "is_valid_syntax_isegment", - "is_valid_syntax_isegment_nz", - "is_valid_syntax_isegment_nz_nc", - "is_valid_syntax_ipchar", - "is_valid_syntax_iquery", - "is_valid_syntax_ifragment", - "is_valid_syntax_iunreserved", - "is_valid_syntax_ucschar", - "is_valid_syntax_iprivate", - "is_valid_syntax_sub_delims", - "is_valid_syntax_ip_literal", - "is_valid_syntax_ipvfuture", - "is_valid_syntax_ipv6address", - "is_valid_syntax_h16", - "is_valid_syntax_ls32", - "is_valid_syntax_ipv4address", - "is_valid_syntax_dec_octet", - "is_valid_syntax_digit", - "is_valid_syntax_non_zero", - "is_valid_syntax_unreserved", - "is_valid_syntax_alpha", - "is_valid_syntax_hexdig", - "is_valid_syntax_port", - "is_valid_syntax_pct_encoded", -] RFC3987_SYNTAX_PARSER_TYPE: str = "earley" @@ -320,8 +268,6 @@ 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': @@ -330,6 +276,6 @@ def __getattr__(name: str) -> Any: def __dir__() -> list[str]: - return sorted(set(globals().keys()) | set(__all__)) + return sorted(set(globals().keys()) | {'grammar', 'syntax_parser'}) # endregion lazy loaded attrs From 7de6928b7c25f103620e25e451f1c66efe1dc798 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 21 Jul 2026 15:17:20 +0200 Subject: [PATCH 02/11] perf: lazy loading Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index f18eebd..acd5563 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -272,8 +272,7 @@ def __getattr__(name: str) -> Any: return _get_grammar() if name == 'syntax_parser': return _get_syntax_parser() - raise AttributeError(f"module {__name__!r} failed to implement attribute {name!r}") - + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") def __dir__() -> list[str]: return sorted(set(globals().keys()) | {'grammar', 'syntax_parser'}) From ab6f53eae43a14a743502299b9811ba6abfe2841 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 21 Jul 2026 15:20:01 +0200 Subject: [PATCH 03/11] perf: lazy loading Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/rfc3987_syntax2/__init__.py b/src/rfc3987_syntax2/__init__.py index a0e77f8..61f833c 100644 --- a/src/rfc3987_syntax2/__init__.py +++ b/src/rfc3987_syntax2/__init__.py @@ -60,9 +60,14 @@ ] +_EXPORTS = frozenset(__all__) + + def __getattr__(name: str) -> Any: - if name in __all__: - return getattr(_syntax_helpers, name) + if name in _EXPORTS: + value = getattr(_syntax_helpers, name) + globals()[name] = value + return value raise AttributeError(f"module {__name__!r} has no attribute {name!r}") From 914cbed84bcd13a7ff08280f70a2c94139500459 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 21 Jul 2026 15:20:19 +0200 Subject: [PATCH 04/11] perf: lazy loading Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rfc3987_syntax2/__init__.py b/src/rfc3987_syntax2/__init__.py index 61f833c..abcc679 100644 --- a/src/rfc3987_syntax2/__init__.py +++ b/src/rfc3987_syntax2/__init__.py @@ -59,7 +59,6 @@ "is_valid_syntax_pct_encoded", ] - _EXPORTS = frozenset(__all__) From 99f4ffb8e5b8bf144cec8e82101979712cfd5dc5 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 21 Jul 2026 15:33:52 +0200 Subject: [PATCH 05/11] perf: lazy loading Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/__init__.py | 57 ++------------------------- src/rfc3987_syntax2/syntax_helpers.py | 54 ++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/src/rfc3987_syntax2/__init__.py b/src/rfc3987_syntax2/__init__.py index abcc679..3de7209 100644 --- a/src/rfc3987_syntax2/__init__.py +++ b/src/rfc3987_syntax2/__init__.py @@ -6,64 +6,13 @@ from . import syntax_helpers as _syntax_helpers -__all__ = [ - "RFC3987_SYNTAX_PARSER_TYPE", - "RFC3987_SYNTAX_GRAMMAR_PATH", - "RFC3987_SYNTAX_TERMS", - "grammar", - "syntax_parser", - "parse", - "is_valid_syntax", - "make_syntax_validator", - "RFC3987_SYNTAX_TERM_VALIDATORS", - "is_valid_syntax_iri", - "is_valid_syntax_iri_reference", - "is_valid_syntax_absolute_iri", - "is_valid_syntax_scheme", - "is_valid_syntax_ihier_part", - "is_valid_syntax_irelative_ref", - "is_valid_syntax_irelative_part", - "is_valid_syntax_iauthority", - "is_valid_syntax_iuserinfo", - "is_valid_syntax_ihost", - "is_valid_syntax_ireg_name", - "is_valid_syntax_ipath", - "is_valid_syntax_ipath_abempty", - "is_valid_syntax_ipath_absolute", - "is_valid_syntax_ipath_noscheme", - "is_valid_syntax_ipath_rootless", - "is_valid_syntax_ipath_empty", - "is_valid_syntax_isegment", - "is_valid_syntax_isegment_nz", - "is_valid_syntax_isegment_nz_nc", - "is_valid_syntax_ipchar", - "is_valid_syntax_iquery", - "is_valid_syntax_ifragment", - "is_valid_syntax_iunreserved", - "is_valid_syntax_ucschar", - "is_valid_syntax_iprivate", - "is_valid_syntax_sub_delims", - "is_valid_syntax_ip_literal", - "is_valid_syntax_ipvfuture", - "is_valid_syntax_ipv6address", - "is_valid_syntax_h16", - "is_valid_syntax_ls32", - "is_valid_syntax_ipv4address", - "is_valid_syntax_dec_octet", - "is_valid_syntax_digit", - "is_valid_syntax_non_zero", - "is_valid_syntax_unreserved", - "is_valid_syntax_alpha", - "is_valid_syntax_hexdig", - "is_valid_syntax_port", - "is_valid_syntax_pct_encoded", -] +__all__ = _syntax_helpers.__all__ -_EXPORTS = frozenset(__all__) +_EXPORTS_SH = frozenset(_syntax_helpers.__all__) def __getattr__(name: str) -> Any: - if name in _EXPORTS: + if name in _EXPORTS_SH: value = getattr(_syntax_helpers, name) globals()[name] = value return value diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index acd5563..ad404cc 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -12,6 +12,58 @@ from .utils import load_grammar +__all__ = [ + "RFC3987_SYNTAX_PARSER_TYPE", + "RFC3987_SYNTAX_GRAMMAR_PATH", + "RFC3987_SYNTAX_TERMS", + "grammar", + "syntax_parser", + "parse", + "is_valid_syntax", + "make_syntax_validator", + "RFC3987_SYNTAX_TERM_VALIDATORS", + "is_valid_syntax_iri", + "is_valid_syntax_iri_reference", + "is_valid_syntax_absolute_iri", + "is_valid_syntax_scheme", + "is_valid_syntax_ihier_part", + "is_valid_syntax_irelative_ref", + "is_valid_syntax_irelative_part", + "is_valid_syntax_iauthority", + "is_valid_syntax_iuserinfo", + "is_valid_syntax_ihost", + "is_valid_syntax_ireg_name", + "is_valid_syntax_ipath", + "is_valid_syntax_ipath_abempty", + "is_valid_syntax_ipath_absolute", + "is_valid_syntax_ipath_noscheme", + "is_valid_syntax_ipath_rootless", + "is_valid_syntax_ipath_empty", + "is_valid_syntax_isegment", + "is_valid_syntax_isegment_nz", + "is_valid_syntax_isegment_nz_nc", + "is_valid_syntax_ipchar", + "is_valid_syntax_iquery", + "is_valid_syntax_ifragment", + "is_valid_syntax_iunreserved", + "is_valid_syntax_ucschar", + "is_valid_syntax_iprivate", + "is_valid_syntax_sub_delims", + "is_valid_syntax_ip_literal", + "is_valid_syntax_ipvfuture", + "is_valid_syntax_ipv6address", + "is_valid_syntax_h16", + "is_valid_syntax_ls32", + "is_valid_syntax_ipv4address", + "is_valid_syntax_dec_octet", + "is_valid_syntax_digit", + "is_valid_syntax_non_zero", + "is_valid_syntax_unreserved", + "is_valid_syntax_alpha", + "is_valid_syntax_hexdig", + "is_valid_syntax_port", + "is_valid_syntax_pct_encoded", +] RFC3987_SYNTAX_PARSER_TYPE: str = "earley" RFC3987_SYNTAX_GRAMMAR_PATH: Path = Path(__file__).parent / "syntax_rfc3987.lark" @@ -275,6 +327,6 @@ def __getattr__(name: str) -> Any: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") def __dir__() -> list[str]: - return sorted(set(globals().keys()) | {'grammar', 'syntax_parser'}) + return sorted(set(globals().keys()) | set(__all__)) # endregion lazy loaded attrs From 5f6cf819688ff9d670b51e9b9489ff33b2d205d8 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 21 Jul 2026 15:47:55 +0200 Subject: [PATCH 06/11] perf: lazy loading Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index ad404cc..9549338 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -321,9 +321,13 @@ def _get_syntax_parser() -> Lark: def __getattr__(name: str) -> Any: if name == 'grammar': - return _get_grammar() + value = _get_grammar() + globals()[name] = value + return value if name == 'syntax_parser': - return _get_syntax_parser() + value = _get_syntax_parser() + globals()[name] = value + return value raise AttributeError(f"module {__name__!r} has no attribute {name!r}") def __dir__() -> list[str]: From bc2da4f0f1f6f1664a1b377135dce3c5b11c0df3 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 21 Jul 2026 15:52:03 +0200 Subject: [PATCH 07/11] perf: lazy loading Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index 9549338..fe7d01e 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -321,13 +321,13 @@ def _get_syntax_parser() -> Lark: def __getattr__(name: str) -> Any: if name == 'grammar': - value = _get_grammar() - globals()[name] = value - return value + global grammar + grammar = _get_grammar() + return grammar if name == 'syntax_parser': - value = _get_syntax_parser() - globals()[name] = value - return value + 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]: From 228fd0322ffbe0e9a33ecca869349d04d94311e7 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 21 Jul 2026 15:56:32 +0200 Subject: [PATCH 08/11] perf: lazy loading Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index fe7d01e..9b82840 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -320,11 +320,11 @@ def _get_syntax_parser() -> Lark: def __getattr__(name: str) -> Any: - if name == 'grammar': + if name == "grammar": global grammar grammar = _get_grammar() return grammar - if name == 'syntax_parser': + if name == "syntax_parser": global syntax_parser syntax_parser = _get_syntax_parser() return syntax_parser From 2141a5ed1fe0c177167803a74755b561c0abb5d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:56:55 +0000 Subject: [PATCH 09/11] test: add regression tests for no-eager-loading on import --- tests/test_lazy_attrs.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_lazy_attrs.py b/tests/test_lazy_attrs.py index 2267ca2..0e13b88 100644 --- a/tests/test_lazy_attrs.py +++ b/tests/test_lazy_attrs.py @@ -1,10 +1,43 @@ # 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) From a9739530a51b04ea71f10fd428c971a4c60ab409 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:57:33 +0000 Subject: [PATCH 10/11] test: improve subprocess command string readability in lazy tests --- tests/test_lazy_attrs.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/test_lazy_attrs.py b/tests/test_lazy_attrs.py index 0e13b88..29266a4 100644 --- a/tests/test_lazy_attrs.py +++ b/tests/test_lazy_attrs.py @@ -11,12 +11,7 @@ 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"', - ], + [sys.executable, '-c', 'import rfc3987_syntax2; assert "grammar" not in vars(rfc3987_syntax2), "grammar was eagerly loaded on import"'], capture_output=True, text=True, ) @@ -26,12 +21,7 @@ def test_grammar_not_eager() -> None: 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"', - ], + [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, ) From b8343039e3d79c90ccb6595ffab1e1284c55aa57 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 21 Jul 2026 16:07:00 +0200 Subject: [PATCH 11/11] docs Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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