From 310cc75d3b2eeda10799e3df80e29bbf7191622d Mon Sep 17 00:00:00 2001 From: Robert Lechte Date: Thu, 4 Sep 2025 22:25:19 +1000 Subject: [PATCH 01/11] Lazy-load syntax validators on attr access --- src/rfc3987_syntax/syntax_helpers.py | 87 ++++++++-------------------- 1 file changed, 23 insertions(+), 64 deletions(-) diff --git a/src/rfc3987_syntax/syntax_helpers.py b/src/rfc3987_syntax/syntax_helpers.py index fcb3f32..6b88bf6 100644 --- a/src/rfc3987_syntax/syntax_helpers.py +++ b/src/rfc3987_syntax/syntax_helpers.py @@ -75,78 +75,37 @@ def syntax_validator(text): return syntax_validator -is_valid_syntax_iri = make_syntax_validator("iri") +# Cache for lazily created validators +_validator_cache = {} -is_valid_syntax_iri_reference = make_syntax_validator("iri_reference") -is_valid_syntax_absolute_iri = make_syntax_validator("absolute_iri") +SYNTAX_VALIDATOR_PREFIX = "is_valid_syntax_" -is_valid_syntax_irelative_ref = make_syntax_validator("irelative_ref") -is_valid_syntax_irelative_part = make_syntax_validator("irelative_part") +def get_syntax_validator(attr_name: str): + if not attr_name.startswith(SYNTAX_VALIDATOR_PREFIX): + return None -is_valid_syntax_ihier_part = make_syntax_validator("ihier_part") + term_name = attr_name[len(SYNTAX_VALIDATOR_PREFIX) :] -is_valid_syntax_iauthority = make_syntax_validator("iauthority") + if term_name not in RFC3987_SYNTAX_TERMS: + return None -is_valid_syntax_iuserinfo = make_syntax_validator("iuserinfo") + return make_syntax_validator(term_name) -is_valid_syntax_ihost = make_syntax_validator("ihost") -is_valid_syntax_ireg_name = make_syntax_validator("ireg_name") +def __getattr__(name): + """ + Lazily create syntax validators when accessed. -is_valid_syntax_ipath = make_syntax_validator("ipath") - -is_valid_syntax_ipath_abempty = make_syntax_validator("ipath_abempty") - -is_valid_syntax_ipath_absolute = make_syntax_validator("ipath_absolute") - -is_valid_syntax_ipath_noscheme = make_syntax_validator("ipath_noscheme") - -is_valid_syntax_ipath_rootless = make_syntax_validator("ipath_rootless") - -is_valid_syntax_ipath_empty = make_syntax_validator("ipath_empty") - -is_valid_syntax_isegment = make_syntax_validator("isegment") - -is_valid_syntax_isegment_nz = make_syntax_validator("isegment_nz") - -is_valid_syntax_isegment_nz_nc = make_syntax_validator("isegment_nz_nc") - -is_valid_syntax_ipchar = make_syntax_validator("ipchar") - -is_valid_syntax_iquery = make_syntax_validator("iquery") - -is_valid_syntax_ifragment = make_syntax_validator("ifragment") - -is_valid_syntax_iunreserved = make_syntax_validator("iunreserved") - -is_valid_syntax_ucschar = make_syntax_validator("ucschar") - -is_valid_syntax_iprivate = make_syntax_validator("iprivate") - -is_valid_syntax_sub_delims = make_syntax_validator("sub_delims") - -is_valid_syntax_ip_literal = make_syntax_validator("ip_literal") - -is_valid_syntax_ipvfuture = make_syntax_validator("ipvfuture") - -is_valid_syntax_ipv6address = make_syntax_validator("ipv6address") - -is_valid_syntax_h16 = make_syntax_validator("h16") - -is_valid_syntax_ls32 = make_syntax_validator("ls32") - -is_valid_syntax_ipv4address = make_syntax_validator("ipv4address") - -is_valid_syntax_dec_octet = make_syntax_validator("dec_octet") - -is_valid_syntax_unreserved = make_syntax_validator("unreserved") - -is_valid_syntax_alpha = make_syntax_validator("alpha") - -is_valid_syntax_digit = make_syntax_validator("digit") - -is_valid_syntax_hexdig = make_syntax_validator("hexdig") + When an attribute like 'is_valid_syntax_iri' is accessed, this function + will create and cache the corresponding validator function. + """ + try: + return _validator_cache[name] + except KeyError: + if validator := get_syntax_validator(name): + _validator_cache[name] = validator + return validator -is_valid_syntax_port = make_syntax_validator("port") + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") From 7053a7365c338134f24a3f126986bd71ca877396 Mon Sep 17 00:00:00 2001 From: Robert Lechte Date: Thu, 4 Sep 2025 22:55:22 +1000 Subject: [PATCH 02/11] Also create the syntax parser lazily --- src/rfc3987_syntax/syntax_helpers.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/rfc3987_syntax/syntax_helpers.py b/src/rfc3987_syntax/syntax_helpers.py index 6b88bf6..6e8367e 100644 --- a/src/rfc3987_syntax/syntax_helpers.py +++ b/src/rfc3987_syntax/syntax_helpers.py @@ -1,3 +1,5 @@ +import sys + from lark import Lark, ParseTree, exceptions from pathlib import Path @@ -47,11 +49,9 @@ grammar: str = load_grammar(RFC3987_SYNTAX_GRAMMAR_PATH) -syntax_parser = Lark(grammar, start=["iri", "iri_reference", "absolute_iri"], parser=RFC3987_SYNTAX_PARSER_TYPE) - def parse(term: str, value: str) -> ParseTree: - return syntax_parser.parse(value, start=term) + return sys.modules[__name__].syntax_parser.parse(value, start=term) def is_valid_syntax(term: str, value: str): @@ -76,7 +76,7 @@ def syntax_validator(text): # Cache for lazily created validators -_validator_cache = {} +_attr_cache = {} SYNTAX_VALIDATOR_PREFIX = "is_valid_syntax_" @@ -94,18 +94,27 @@ def get_syntax_validator(attr_name: str): return make_syntax_validator(term_name) +def get_syntax_parser(): + return Lark(grammar, start=["iri", "iri_reference", "absolute_iri"], parser=RFC3987_SYNTAX_PARSER_TYPE) + def __getattr__(name): """ - Lazily create syntax validators when accessed. + Lazily create attributes, in particular syntax validators, when accessed. When an attribute like 'is_valid_syntax_iri' is accessed, this function will create and cache the corresponding validator function. + + We also create the syntax parser lazily. """ try: - return _validator_cache[name] + return _attr_cache[name] except KeyError: + if name == 'syntax_parser': + syntax_parser = get_syntax_parser() + _attr_cache[name] = syntax_parser + return syntax_parser if validator := get_syntax_validator(name): - _validator_cache[name] = validator + _attr_cache[name] = validator return validator raise AttributeError(f"module '{__name__}' has no attribute '{name}'") From 79249cbc4f043428827d06001f5db1f64daf07db Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 12:35:26 +0200 Subject: [PATCH 03/11] revert Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 100 +++++++++++++++++--------- 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index eabfe3f..418c8b7 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -4,8 +4,6 @@ from typing import Callable -import sys - from lark import Lark, ParseTree, exceptions from pathlib import Path @@ -60,9 +58,11 @@ grammar: str = load_grammar(RFC3987_SYNTAX_GRAMMAR_PATH) +syntax_parser = Lark(grammar, start=["iri", "iri_reference", "absolute_iri"], parser=RFC3987_SYNTAX_PARSER_TYPE) + def parse(term: str, value: str) -> ParseTree: - return sys.modules[__name__].syntax_parser.parse(value, start=term) + return syntax_parser.parse(value, start=term) def is_valid_syntax(term: str, value: str) -> bool: @@ -86,46 +86,78 @@ def syntax_validator(text: str) -> bool: return syntax_validator -# Cache for lazily created validators -_attr_cache = {} +is_valid_syntax_iri = make_syntax_validator("iri") +is_valid_syntax_iri_reference = make_syntax_validator("iri_reference") -SYNTAX_VALIDATOR_PREFIX = "is_valid_syntax_" +is_valid_syntax_absolute_iri = make_syntax_validator("absolute_iri") +is_valid_syntax_irelative_ref = make_syntax_validator("irelative_ref") -def get_syntax_validator(attr_name: str): - if not attr_name.startswith(SYNTAX_VALIDATOR_PREFIX): - return None +is_valid_syntax_irelative_part = make_syntax_validator("irelative_part") - term_name = attr_name[len(SYNTAX_VALIDATOR_PREFIX) :] +is_valid_syntax_ihier_part = make_syntax_validator("ihier_part") - if term_name not in RFC3987_SYNTAX_TERMS: - return None +is_valid_syntax_iauthority = make_syntax_validator("iauthority") - return make_syntax_validator(term_name) +is_valid_syntax_iuserinfo = make_syntax_validator("iuserinfo") +is_valid_syntax_ihost = make_syntax_validator("ihost") -def get_syntax_parser(): - return Lark(grammar, start=["iri", "iri_reference", "absolute_iri"], parser=RFC3987_SYNTAX_PARSER_TYPE) +is_valid_syntax_ireg_name = make_syntax_validator("ireg_name") -def __getattr__(name): - """ - Lazily create attributes, in particular syntax validators, when accessed. +is_valid_syntax_ipath = make_syntax_validator("ipath") - When an attribute like 'is_valid_syntax_iri' is accessed, this function - will create and cache the corresponding validator function. +is_valid_syntax_ipath_abempty = make_syntax_validator("ipath_abempty") - We also create the syntax parser lazily. - """ - try: - return _attr_cache[name] - except KeyError: - if name == 'syntax_parser': - syntax_parser = get_syntax_parser() - _attr_cache[name] = syntax_parser - return syntax_parser - if validator := get_syntax_validator(name): - _attr_cache[name] = validator - return validator - - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") +is_valid_syntax_ipath_absolute = make_syntax_validator("ipath_absolute") + +is_valid_syntax_ipath_noscheme = make_syntax_validator("ipath_noscheme") + +is_valid_syntax_ipath_rootless = make_syntax_validator("ipath_rootless") + +is_valid_syntax_ipath_empty = make_syntax_validator("ipath_empty") + +is_valid_syntax_isegment = make_syntax_validator("isegment") + +is_valid_syntax_isegment_nz = make_syntax_validator("isegment_nz") + +is_valid_syntax_isegment_nz_nc = make_syntax_validator("isegment_nz_nc") + +is_valid_syntax_ipchar = make_syntax_validator("ipchar") + +is_valid_syntax_iquery = make_syntax_validator("iquery") + +is_valid_syntax_ifragment = make_syntax_validator("ifragment") + +is_valid_syntax_iunreserved = make_syntax_validator("iunreserved") + +is_valid_syntax_ucschar = make_syntax_validator("ucschar") + +is_valid_syntax_iprivate = make_syntax_validator("iprivate") + +is_valid_syntax_sub_delims = make_syntax_validator("sub_delims") + +is_valid_syntax_ip_literal = make_syntax_validator("ip_literal") + +is_valid_syntax_ipvfuture = make_syntax_validator("ipvfuture") + +is_valid_syntax_ipv6address = make_syntax_validator("ipv6address") + +is_valid_syntax_h16 = make_syntax_validator("h16") + +is_valid_syntax_ls32 = make_syntax_validator("ls32") + +is_valid_syntax_ipv4address = make_syntax_validator("ipv4address") + +is_valid_syntax_dec_octet = make_syntax_validator("dec_octet") + +is_valid_syntax_unreserved = make_syntax_validator("unreserved") + +is_valid_syntax_alpha = make_syntax_validator("alpha") + +is_valid_syntax_digit = make_syntax_validator("digit") + +is_valid_syntax_hexdig = make_syntax_validator("hexdig") + +is_valid_syntax_port = make_syntax_validator("port") From 28647ffd3033943d3524eda31520f9a090114177 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 12:39:19 +0200 Subject: [PATCH 04/11] add __all__ Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index 418c8b7..d25c674 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -10,6 +10,55 @@ 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", + "is_valid_syntax_iri", + "is_valid_syntax_iri_reference", + "is_valid_syntax_absolute_iri", + "is_valid_syntax_irelative_ref", + "is_valid_syntax_irelative_part", + "is_valid_syntax_ihier_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_unreserved", + "is_valid_syntax_alpha", + "is_valid_syntax_digit", + "is_valid_syntax_hexdig", + "is_valid_syntax_port", +] + RFC3987_SYNTAX_PARSER_TYPE: str = "earley" RFC3987_SYNTAX_GRAMMAR_PATH: Path = Path(__file__).parent / "syntax_rfc3987.lark" RFC3987_SYNTAX_TERMS: list[str] = [ From e2b657cd368ba7754210818b315fbdf37e92881b Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 13:40:42 +0200 Subject: [PATCH 05/11] lsazy-load Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 185 ++++++++++++++------------ 1 file changed, 101 insertions(+), 84 deletions(-) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index d25c674..950599c 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -2,7 +2,7 @@ # Copyright (c) 2026 Jan Kowalleck - modifications and maintenance # SPDX-License-Identifier: MIT -from typing import Callable +from typing import Callable, Optional, TYPE_CHECKING from lark import Lark, ParseTree, exceptions @@ -59,6 +59,7 @@ "is_valid_syntax_port", ] + RFC3987_SYNTAX_PARSER_TYPE: str = "earley" RFC3987_SYNTAX_GRAMMAR_PATH: Path = Path(__file__).parent / "syntax_rfc3987.lark" RFC3987_SYNTAX_TERMS: list[str] = [ @@ -105,13 +106,8 @@ "pct_encoded", ] -grammar: str = load_grammar(RFC3987_SYNTAX_GRAMMAR_PATH) - -syntax_parser = Lark(grammar, start=["iri", "iri_reference", "absolute_iri"], parser=RFC3987_SYNTAX_PARSER_TYPE) - - def parse(term: str, value: str) -> ParseTree: - return syntax_parser.parse(value, start=term) + return get_syntax_parser().parse(value, start=term) def is_valid_syntax(term: str, value: str) -> bool: @@ -121,9 +117,12 @@ def is_valid_syntax(term: str, value: str) -> bool: except exceptions.LarkError: return False +T_SYNTAX_VALIDATOR = Callable[[str], bool] -def make_syntax_validator(rule_name: str) -> Callable[[str], bool]: - parser = Lark(grammar, start=rule_name, parser=RFC3987_SYNTAX_PARSER_TYPE) +def make_syntax_validator(rule_name: str) -> T_SYNTAX_VALIDATOR: + parser = Lark(get_grammar(), + start=rule_name, + parser=RFC3987_SYNTAX_PARSER_TYPE) def syntax_validator(text: str) -> bool: try: @@ -135,78 +134,96 @@ def syntax_validator(text: str) -> bool: return syntax_validator -is_valid_syntax_iri = make_syntax_validator("iri") - -is_valid_syntax_iri_reference = make_syntax_validator("iri_reference") - -is_valid_syntax_absolute_iri = make_syntax_validator("absolute_iri") - -is_valid_syntax_irelative_ref = make_syntax_validator("irelative_ref") - -is_valid_syntax_irelative_part = make_syntax_validator("irelative_part") - -is_valid_syntax_ihier_part = make_syntax_validator("ihier_part") - -is_valid_syntax_iauthority = make_syntax_validator("iauthority") - -is_valid_syntax_iuserinfo = make_syntax_validator("iuserinfo") - -is_valid_syntax_ihost = make_syntax_validator("ihost") - -is_valid_syntax_ireg_name = make_syntax_validator("ireg_name") - -is_valid_syntax_ipath = make_syntax_validator("ipath") - -is_valid_syntax_ipath_abempty = make_syntax_validator("ipath_abempty") - -is_valid_syntax_ipath_absolute = make_syntax_validator("ipath_absolute") - -is_valid_syntax_ipath_noscheme = make_syntax_validator("ipath_noscheme") - -is_valid_syntax_ipath_rootless = make_syntax_validator("ipath_rootless") - -is_valid_syntax_ipath_empty = make_syntax_validator("ipath_empty") - -is_valid_syntax_isegment = make_syntax_validator("isegment") - -is_valid_syntax_isegment_nz = make_syntax_validator("isegment_nz") - -is_valid_syntax_isegment_nz_nc = make_syntax_validator("isegment_nz_nc") - -is_valid_syntax_ipchar = make_syntax_validator("ipchar") - -is_valid_syntax_iquery = make_syntax_validator("iquery") - -is_valid_syntax_ifragment = make_syntax_validator("ifragment") - -is_valid_syntax_iunreserved = make_syntax_validator("iunreserved") - -is_valid_syntax_ucschar = make_syntax_validator("ucschar") - -is_valid_syntax_iprivate = make_syntax_validator("iprivate") - -is_valid_syntax_sub_delims = make_syntax_validator("sub_delims") - -is_valid_syntax_ip_literal = make_syntax_validator("ip_literal") - -is_valid_syntax_ipvfuture = make_syntax_validator("ipvfuture") - -is_valid_syntax_ipv6address = make_syntax_validator("ipv6address") - -is_valid_syntax_h16 = make_syntax_validator("h16") - -is_valid_syntax_ls32 = make_syntax_validator("ls32") - -is_valid_syntax_ipv4address = make_syntax_validator("ipv4address") - -is_valid_syntax_dec_octet = make_syntax_validator("dec_octet") - -is_valid_syntax_unreserved = make_syntax_validator("unreserved") - -is_valid_syntax_alpha = make_syntax_validator("alpha") - -is_valid_syntax_digit = make_syntax_validator("digit") - -is_valid_syntax_hexdig = make_syntax_validator("hexdig") - -is_valid_syntax_port = make_syntax_validator("port") +_grammar: Optional[str] = None + + +def get_grammar() -> str: + """this is private API""" + global _grammar + if _grammar is None: + _grammar = load_grammar(RFC3987_SYNTAX_GRAMMAR_PATH) + return _grammar + + +_syntax_parser: Optional[Lark] = None + + +def get_syntax_parser() -> Lark: + """this is private API""" + global _syntax_parser + if _syntax_parser is None: + _syntax_parser = Lark(get_grammar(), + start=["iri", "iri_reference", "absolute_iri"], + parser=RFC3987_SYNTAX_PARSER_TYPE) + return _syntax_parser + + +_CACHE_SYNTAX_VALIDATOR: dict[str, T_SYNTAX_VALIDATOR] = {} + + +def get_syntax_validator(rule_name) -> T_SYNTAX_VALIDATOR: + """this is private API""" + validator = _CACHE_SYNTAX_VALIDATOR.get(rule_name) + if validator is None: + validator = _CACHE_SYNTAX_VALIDATOR[rule_name] = make_syntax_validator(rule_name) + return validator + + +if TYPE_CHECKING: + # define types for all those lazy-loaded symbols + grammar: str + syntax_parser: Lark + is_valid_syntax_iri: T_SYNTAX_VALIDATOR + is_valid_syntax_iri_reference: T_SYNTAX_VALIDATOR + is_valid_syntax_absolute_iri: T_SYNTAX_VALIDATOR + is_valid_syntax_irelative_ref: T_SYNTAX_VALIDATOR + is_valid_syntax_irelative_part: T_SYNTAX_VALIDATOR + is_valid_syntax_ihier_part: T_SYNTAX_VALIDATOR + is_valid_syntax_iauthority: T_SYNTAX_VALIDATOR + is_valid_syntax_iuserinfo: T_SYNTAX_VALIDATOR + is_valid_syntax_ihost: T_SYNTAX_VALIDATOR + is_valid_syntax_ireg_name: T_SYNTAX_VALIDATOR + is_valid_syntax_ipath: T_SYNTAX_VALIDATOR + is_valid_syntax_ipath_abempty: T_SYNTAX_VALIDATOR + is_valid_syntax_ipath_absolute: T_SYNTAX_VALIDATOR + is_valid_syntax_ipath_noscheme: T_SYNTAX_VALIDATOR + is_valid_syntax_ipath_rootless: T_SYNTAX_VALIDATOR + is_valid_syntax_ipath_empty: T_SYNTAX_VALIDATOR + is_valid_syntax_isegment: T_SYNTAX_VALIDATOR + is_valid_syntax_isegment_nz: T_SYNTAX_VALIDATOR + is_valid_syntax_isegment_nz_nc: T_SYNTAX_VALIDATOR + is_valid_syntax_ipchar: T_SYNTAX_VALIDATOR + is_valid_syntax_iquery: T_SYNTAX_VALIDATOR + is_valid_syntax_ifragment: T_SYNTAX_VALIDATOR + is_valid_syntax_iunreserved: T_SYNTAX_VALIDATOR + is_valid_syntax_ucschar: T_SYNTAX_VALIDATOR + is_valid_syntax_iprivate: T_SYNTAX_VALIDATOR + is_valid_syntax_sub_delims: T_SYNTAX_VALIDATOR + is_valid_syntax_ip_literal: T_SYNTAX_VALIDATOR + is_valid_syntax_ipvfuture: T_SYNTAX_VALIDATOR + is_valid_syntax_ipv6address: T_SYNTAX_VALIDATOR + is_valid_syntax_h16: T_SYNTAX_VALIDATOR + is_valid_syntax_ls32: T_SYNTAX_VALIDATOR + is_valid_syntax_ipv4address: T_SYNTAX_VALIDATOR + is_valid_syntax_dec_octet: T_SYNTAX_VALIDATOR + is_valid_syntax_unreserved: T_SYNTAX_VALIDATOR + is_valid_syntax_alpha: T_SYNTAX_VALIDATOR + is_valid_syntax_digit: T_SYNTAX_VALIDATOR + is_valid_syntax_hexdig: T_SYNTAX_VALIDATOR + is_valid_syntax_port: T_SYNTAX_VALIDATOR + + +def __getattr__(name: str): + 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() + if name.startswith('is_valid_syntax_'): + return get_syntax_validator(name[16:]) # the prefix is 16 chars long + raise NotImplementedError(f"module {__name__!r} failed to implement attribute {name!r}") + + +def __dir__() -> list[str]: + return sorted(list(globals().keys()) + __all__) From 90f515fc1320eebdfc2a8d5342343280535839cf Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 14:43:15 +0200 Subject: [PATCH 06/11] lsazy-load Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 160 +++++++++++++++----------- 1 file changed, 95 insertions(+), 65 deletions(-) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index 950599c..136f947 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -2,7 +2,7 @@ # Copyright (c) 2026 Jan Kowalleck - modifications and maintenance # SPDX-License-Identifier: MIT -from typing import Callable, Optional, TYPE_CHECKING +from typing import Any, Callable, Optional, TYPE_CHECKING from lark import Lark, ParseTree, exceptions @@ -106,8 +106,8 @@ "pct_encoded", ] -def parse(term: str, value: str) -> ParseTree: - return get_syntax_parser().parse(value, start=term) +def parse(term: str, value: str) -> 'ParseTree': + return _get_syntax_parser().parse(value, start=term) def is_valid_syntax(term: str, value: str) -> bool: @@ -117,14 +117,16 @@ def is_valid_syntax(term: str, value: str) -> bool: except exceptions.LarkError: return False -T_SYNTAX_VALIDATOR = Callable[[str], bool] -def make_syntax_validator(rule_name: str) -> T_SYNTAX_VALIDATOR: - parser = Lark(get_grammar(), - start=rule_name, - parser=RFC3987_SYNTAX_PARSER_TYPE) +def make_syntax_validator(rule_name: str) -> Callable[[str], bool]: + parser: Optional[Lark] = None def syntax_validator(text: str) -> bool: + nonlocal parser + if parser is None: + parser = Lark(_get_grammar(), + start=rule_name, + parser=RFC3987_SYNTAX_PARSER_TYPE) try: parser.parse(text) return True @@ -137,7 +139,7 @@ def syntax_validator(text: str) -> bool: _grammar: Optional[str] = None -def get_grammar() -> str: +def _get_grammar() -> str: """this is private API""" global _grammar if _grammar is None: @@ -148,82 +150,110 @@ def get_grammar() -> str: _syntax_parser: Optional[Lark] = None -def get_syntax_parser() -> Lark: +def _get_syntax_parser() -> Lark: """this is private API""" global _syntax_parser if _syntax_parser is None: - _syntax_parser = Lark(get_grammar(), + _syntax_parser = Lark(_get_grammar(), start=["iri", "iri_reference", "absolute_iri"], parser=RFC3987_SYNTAX_PARSER_TYPE) return _syntax_parser -_CACHE_SYNTAX_VALIDATOR: dict[str, T_SYNTAX_VALIDATOR] = {} +is_valid_syntax_iri = make_syntax_validator("iri") +is_valid_syntax_iri_reference = make_syntax_validator("iri_reference") -def get_syntax_validator(rule_name) -> T_SYNTAX_VALIDATOR: - """this is private API""" - validator = _CACHE_SYNTAX_VALIDATOR.get(rule_name) - if validator is None: - validator = _CACHE_SYNTAX_VALIDATOR[rule_name] = make_syntax_validator(rule_name) - return validator +is_valid_syntax_absolute_iri = make_syntax_validator("absolute_iri") + +is_valid_syntax_irelative_ref = make_syntax_validator("irelative_ref") + +is_valid_syntax_irelative_part = make_syntax_validator("irelative_part") + +is_valid_syntax_ihier_part = make_syntax_validator("ihier_part") + +is_valid_syntax_iauthority = make_syntax_validator("iauthority") + +is_valid_syntax_iuserinfo = make_syntax_validator("iuserinfo") + +is_valid_syntax_ihost = make_syntax_validator("ihost") + +is_valid_syntax_ireg_name = make_syntax_validator("ireg_name") + +is_valid_syntax_ipath = make_syntax_validator("ipath") + +is_valid_syntax_ipath_abempty = make_syntax_validator("ipath_abempty") + +is_valid_syntax_ipath_absolute = make_syntax_validator("ipath_absolute") + +is_valid_syntax_ipath_noscheme = make_syntax_validator("ipath_noscheme") + +is_valid_syntax_ipath_rootless = make_syntax_validator("ipath_rootless") + +is_valid_syntax_ipath_empty = make_syntax_validator("ipath_empty") + +is_valid_syntax_isegment = make_syntax_validator("isegment") + +is_valid_syntax_isegment_nz = make_syntax_validator("isegment_nz") + +is_valid_syntax_isegment_nz_nc = make_syntax_validator("isegment_nz_nc") + +is_valid_syntax_ipchar = make_syntax_validator("ipchar") + +is_valid_syntax_iquery = make_syntax_validator("iquery") + +is_valid_syntax_ifragment = make_syntax_validator("ifragment") +is_valid_syntax_iunreserved = make_syntax_validator("iunreserved") -if TYPE_CHECKING: - # define types for all those lazy-loaded symbols +is_valid_syntax_ucschar = make_syntax_validator("ucschar") + +is_valid_syntax_iprivate = make_syntax_validator("iprivate") + +is_valid_syntax_sub_delims = make_syntax_validator("sub_delims") + +is_valid_syntax_ip_literal = make_syntax_validator("ip_literal") + +is_valid_syntax_ipvfuture = make_syntax_validator("ipvfuture") + +is_valid_syntax_ipv6address = make_syntax_validator("ipv6address") + +is_valid_syntax_h16 = make_syntax_validator("h16") + +is_valid_syntax_ls32 = make_syntax_validator("ls32") + +is_valid_syntax_ipv4address = make_syntax_validator("ipv4address") + +is_valid_syntax_dec_octet = make_syntax_validator("dec_octet") + +is_valid_syntax_unreserved = make_syntax_validator("unreserved") + +is_valid_syntax_alpha = make_syntax_validator("alpha") + +is_valid_syntax_digit = make_syntax_validator("digit") + +is_valid_syntax_hexdig = make_syntax_validator("hexdig") + +is_valid_syntax_port = make_syntax_validator("port") + +# region lazy loaded attrs + +if TYPE_CHECKING: # types for lazy-loaded symbols grammar: str syntax_parser: Lark - is_valid_syntax_iri: T_SYNTAX_VALIDATOR - is_valid_syntax_iri_reference: T_SYNTAX_VALIDATOR - is_valid_syntax_absolute_iri: T_SYNTAX_VALIDATOR - is_valid_syntax_irelative_ref: T_SYNTAX_VALIDATOR - is_valid_syntax_irelative_part: T_SYNTAX_VALIDATOR - is_valid_syntax_ihier_part: T_SYNTAX_VALIDATOR - is_valid_syntax_iauthority: T_SYNTAX_VALIDATOR - is_valid_syntax_iuserinfo: T_SYNTAX_VALIDATOR - is_valid_syntax_ihost: T_SYNTAX_VALIDATOR - is_valid_syntax_ireg_name: T_SYNTAX_VALIDATOR - is_valid_syntax_ipath: T_SYNTAX_VALIDATOR - is_valid_syntax_ipath_abempty: T_SYNTAX_VALIDATOR - is_valid_syntax_ipath_absolute: T_SYNTAX_VALIDATOR - is_valid_syntax_ipath_noscheme: T_SYNTAX_VALIDATOR - is_valid_syntax_ipath_rootless: T_SYNTAX_VALIDATOR - is_valid_syntax_ipath_empty: T_SYNTAX_VALIDATOR - is_valid_syntax_isegment: T_SYNTAX_VALIDATOR - is_valid_syntax_isegment_nz: T_SYNTAX_VALIDATOR - is_valid_syntax_isegment_nz_nc: T_SYNTAX_VALIDATOR - is_valid_syntax_ipchar: T_SYNTAX_VALIDATOR - is_valid_syntax_iquery: T_SYNTAX_VALIDATOR - is_valid_syntax_ifragment: T_SYNTAX_VALIDATOR - is_valid_syntax_iunreserved: T_SYNTAX_VALIDATOR - is_valid_syntax_ucschar: T_SYNTAX_VALIDATOR - is_valid_syntax_iprivate: T_SYNTAX_VALIDATOR - is_valid_syntax_sub_delims: T_SYNTAX_VALIDATOR - is_valid_syntax_ip_literal: T_SYNTAX_VALIDATOR - is_valid_syntax_ipvfuture: T_SYNTAX_VALIDATOR - is_valid_syntax_ipv6address: T_SYNTAX_VALIDATOR - is_valid_syntax_h16: T_SYNTAX_VALIDATOR - is_valid_syntax_ls32: T_SYNTAX_VALIDATOR - is_valid_syntax_ipv4address: T_SYNTAX_VALIDATOR - is_valid_syntax_dec_octet: T_SYNTAX_VALIDATOR - is_valid_syntax_unreserved: T_SYNTAX_VALIDATOR - is_valid_syntax_alpha: T_SYNTAX_VALIDATOR - is_valid_syntax_digit: T_SYNTAX_VALIDATOR - is_valid_syntax_hexdig: T_SYNTAX_VALIDATOR - is_valid_syntax_port: T_SYNTAX_VALIDATOR - - -def __getattr__(name: str): + + +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() + return _get_grammar() if name == 'syntax_parser': - return get_syntax_parser() - if name.startswith('is_valid_syntax_'): - return get_syntax_validator(name[16:]) # the prefix is 16 chars long + return _get_syntax_parser() raise NotImplementedError(f"module {__name__!r} failed to implement attribute {name!r}") def __dir__() -> list[str]: return sorted(list(globals().keys()) + __all__) + +# endregion lazy loaded attrs From cd78af23c2a38c6a9b495dbdded1181b627f18e4 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 14:58:55 +0200 Subject: [PATCH 07/11] locks Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 39 ++++++++++++++++----------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index 136f947..ea09d64 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -2,12 +2,12 @@ # Copyright (c) 2026 Jan Kowalleck - modifications and maintenance # SPDX-License-Identifier: MIT +from pathlib import Path +from threading import Lock from typing import Any, Callable, Optional, TYPE_CHECKING from lark import Lark, ParseTree, exceptions -from pathlib import Path - from .utils import load_grammar __all__ = [ @@ -120,13 +120,15 @@ def is_valid_syntax(term: str, value: str) -> bool: def make_syntax_validator(rule_name: str) -> Callable[[str], bool]: parser: Optional[Lark] = None + parser_lock = Lock() def syntax_validator(text: str) -> bool: nonlocal parser - if parser is None: - parser = Lark(_get_grammar(), - start=rule_name, - parser=RFC3987_SYNTAX_PARSER_TYPE) + with parser_lock: + if parser is None: + parser = Lark(_get_grammar(), + start=rule_name, + parser=RFC3987_SYNTAX_PARSER_TYPE) try: parser.parse(text) return True @@ -137,27 +139,32 @@ def syntax_validator(text: str) -> bool: _grammar: Optional[str] = None - +_grammar_lock = Lock() def _get_grammar() -> str: """this is private API""" global _grammar - if _grammar is None: - _grammar = load_grammar(RFC3987_SYNTAX_GRAMMAR_PATH) - return _grammar + with _grammar_lock: + if _grammar is None: + _grammar = load_grammar(RFC3987_SYNTAX_GRAMMAR_PATH) + return _grammar _syntax_parser: Optional[Lark] = None - +_syntax_parser_lock = Lock() def _get_syntax_parser() -> Lark: """this is private API""" global _syntax_parser - if _syntax_parser is None: - _syntax_parser = Lark(_get_grammar(), - start=["iri", "iri_reference", "absolute_iri"], - parser=RFC3987_SYNTAX_PARSER_TYPE) - return _syntax_parser + if _syntax_parser is not None: + return _syntax_parser + grammar = _get_grammar() + with _syntax_parser_lock: + if _syntax_parser is None: + _syntax_parser = Lark(grammar, + start=["iri", "iri_reference", "absolute_iri"], + parser=RFC3987_SYNTAX_PARSER_TYPE) + return _syntax_parser is_valid_syntax_iri = make_syntax_validator("iri") From ffa216ffeef26a6f66e99c87281e01648016af21 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 15:01:26 +0200 Subject: [PATCH 08/11] docs Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a806772..81929df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,10 @@ All notable changes to this project will be documented in this file. * Added * Added items to `RFC3987_SYNTAX_TERMS` (via [#41]) - +* Performance: + * publicsymbols use lazyloading (via [#43]) [#41]: https://github.com/jkowalleck/rfc3987-syntax2/pull/41 +[#43]: https://github.com/jkowalleck/rfc3987-syntax2/pull/43 ## v1.2.0 - 2026-07-16 From cb166c462b360a3d00d1287642c2030fdef26784 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 15:46:41 +0200 Subject: [PATCH 09/11] imp 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 ea09d64..ecdb943 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -257,10 +257,10 @@ def __getattr__(name: str) -> Any: return _get_grammar() if name == 'syntax_parser': return _get_syntax_parser() - raise NotImplementedError(f"module {__name__!r} failed to implement attribute {name!r}") + raise AttributeError(f"module {__name__!r} failed to implement attribute {name!r}") def __dir__() -> list[str]: - return sorted(list(globals().keys()) + __all__) + return sorted(set(globals().keys()) | set(__all__)) # endregion lazy loaded attrs From 34f8a1462f28f8856e15dc091be17cfdf388b416 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 15:48:45 +0200 Subject: [PATCH 10/11] docs Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 4 ++-- tests/test_exports_exists.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 tests/test_exports_exists.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 81929df..c6702bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,8 @@ All notable changes to this project will be documented in this file. * Added * Added items to `RFC3987_SYNTAX_TERMS` (via [#41]) -* Performance: - * publicsymbols use lazyloading (via [#43]) +* Performance + * Public symbols use lazy loading (via [#43]) [#41]: https://github.com/jkowalleck/rfc3987-syntax2/pull/41 [#43]: https://github.com/jkowalleck/rfc3987-syntax2/pull/43 diff --git a/tests/test_exports_exists.py b/tests/test_exports_exists.py new file mode 100644 index 0000000..5ba00ac --- /dev/null +++ b/tests/test_exports_exists.py @@ -0,0 +1,6 @@ +# Copyright (c) 2025 Will Riley - original author +# Copyright (c) 2026 Jan Kowalleck - modifications and maintenance +# SPDX-License-Identifier: MIT + +import rfc3987_syntax2 as sut + From 8a1f79ccc7f3ba469fc07320ad4a74a1c90d5e55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:49:49 +0000 Subject: [PATCH 11/11] test: add regression tests for lazy-loaded grammar and syntax_parser attributes --- tests/test_lazy_attrs.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/test_lazy_attrs.py diff --git a/tests/test_lazy_attrs.py b/tests/test_lazy_attrs.py new file mode 100644 index 0000000..2267ca2 --- /dev/null +++ b/tests/test_lazy_attrs.py @@ -0,0 +1,26 @@ +# Copyright (c) 2026 Jan Kowalleck - modifications and maintenance +# SPDX-License-Identifier: MIT + +import rfc3987_syntax2 as sut +from lark import Lark + + +def test_grammar_is_accessible() -> None: + """Regression test: grammar is lazily loaded and accessible as a module attribute.""" + assert isinstance(sut.grammar, str) + assert len(sut.grammar) > 0 + + +def test_syntax_parser_is_accessible() -> None: + """Regression test: syntax_parser is lazily loaded and accessible as a module attribute.""" + assert isinstance(sut.syntax_parser, Lark) + + +def test_grammar_in_dir() -> None: + """Regression test: grammar appears in dir(rfc3987_syntax2).""" + assert "grammar" in dir(sut) + + +def test_syntax_parser_in_dir() -> None: + """Regression test: syntax_parser appears in dir(rfc3987_syntax2).""" + assert "syntax_parser" in dir(sut)