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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file.
* Function `is_valid_syntax_scheme` (via [#48])
* Function `is_valid_syntax_non_zero` (via [#48])
* Function `is_valid_syntax_pct_encoded` (via [#48])
* Dict `RFC3987_SYNTAX_TERM_VALIDATORS` mapping `RFC3987_SYNTAX_TERMS` to validator function (via [#49])
* Performance
* Public symbols use lazy loading (via [#43])
* Tests
Expand All @@ -26,6 +27,7 @@ All notable changes to this project will be documented in this file.
[#43]: https://github.com/jkowalleck/rfc3987-syntax2/pull/43
[#45]: https://github.com/jkowalleck/rfc3987-syntax2/pull/45
[#48]: https://github.com/jkowalleck/rfc3987-syntax2/pull/48
[#49]: https://github.com/jkowalleck/rfc3987-syntax2/pull/49

## v1.2.0 - 2026-07-16

Expand Down
49 changes: 48 additions & 1 deletion src/rfc3987_syntax2/syntax_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"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",
Expand Down Expand Up @@ -120,8 +121,9 @@ 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]:
def make_syntax_validator(rule_name: str) -> _T_SYNTAX_VALIDATOR:
parser: Optional[Lark] = None
parser_lock = Lock()

Expand Down Expand Up @@ -252,6 +254,51 @@ def _get_syntax_parser() -> Lark:

is_valid_syntax_pct_encoded = make_syntax_validator("pct_encoded")

RFC3987_SYNTAX_TERM_VALIDATORS: dict[str, _T_SYNTAX_VALIDATOR] = { # frozendict
"iri": is_valid_syntax_iri,
"iri_reference": is_valid_syntax_iri_reference,
"absolute_iri": is_valid_syntax_absolute_iri,
"scheme": is_valid_syntax_scheme,
"ihier_part": is_valid_syntax_ihier_part,
"irelative_ref": is_valid_syntax_irelative_ref,
"irelative_part": is_valid_syntax_irelative_part,
"iauthority": is_valid_syntax_iauthority,
"iuserinfo": is_valid_syntax_iuserinfo,
"ihost": is_valid_syntax_ihost,
"ireg_name": is_valid_syntax_ireg_name,
"ipath": is_valid_syntax_ipath,
"ipath_abempty": is_valid_syntax_ipath_abempty,
"ipath_absolute": is_valid_syntax_ipath_absolute,
"ipath_noscheme": is_valid_syntax_ipath_noscheme,
"ipath_rootless": is_valid_syntax_ipath_rootless,
"ipath_empty": is_valid_syntax_ipath_empty,
"isegment": is_valid_syntax_isegment,
"isegment_nz": is_valid_syntax_isegment_nz,
"isegment_nz_nc": is_valid_syntax_isegment_nz_nc,
"ipchar": is_valid_syntax_ipchar,
"iquery": is_valid_syntax_iquery,
"ifragment": is_valid_syntax_ifragment,
"iunreserved": is_valid_syntax_iunreserved,
"ucschar": is_valid_syntax_ucschar,
"iprivate": is_valid_syntax_iprivate,
"sub_delims": is_valid_syntax_sub_delims,
"ip_literal": is_valid_syntax_ip_literal,
"ipvfuture": is_valid_syntax_ipvfuture,
"ipv6address": is_valid_syntax_ipv6address,
"h16": is_valid_syntax_h16,
"ls32": is_valid_syntax_ls32,
"ipv4address": is_valid_syntax_ipv4address,
"dec_octet": is_valid_syntax_dec_octet,
"digit": is_valid_syntax_digit,
"non_zero": is_valid_syntax_non_zero,
"unreserved": is_valid_syntax_unreserved,
"alpha": is_valid_syntax_alpha,
"hexdig": is_valid_syntax_hexdig,
"port": is_valid_syntax_port,
"pct_encoded": is_valid_syntax_pct_encoded,
}


# region lazy loaded attrs

if TYPE_CHECKING: # types for lazy-loaded symbols
Expand Down
7 changes: 3 additions & 4 deletions tests/test_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
# Copyright (c) 2026 Jan Kowalleck - modifications and maintenance
# SPDX-License-Identifier: MIT

from typing import TYPE_CHECKING, Any, Callable
from typing import Any, Callable

import pytest

import rfc3987_syntax2 as sut

from . import (
TESTS_DATA_FILES,
valid_syntax_data,
invalid_syntax_data,
SyntaxCase,
Expand All @@ -25,7 +24,7 @@ def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]) -> Any:

@pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data))
def test_is_valid_syntax(term: str, valid_example: SyntaxCase) -> None:
actual = getattr(sut, f'is_valid_syntax_{term}')(valid_example["value"])
actual = sut.RFC3987_SYNTAX_TERM_VALIDATORS[term](valid_example["value"])
print("")
print(
f"Testing {term} with {valid_example['value']} : {valid_example['reason']}"
Expand All @@ -37,7 +36,7 @@ def test_is_valid_syntax(term: str, valid_example: SyntaxCase) -> None:

@pytest.mark.parametrize("term,invalid_example", syntax_data_as_params(invalid_syntax_data))
def test_not_is_valid_syntax(term: str, invalid_example: SyntaxCase) -> None:
actual = getattr(sut, f'is_valid_syntax_{term}')(invalid_example["value"])
actual = sut.RFC3987_SYNTAX_TERM_VALIDATORS[term](invalid_example["value"])
print("")
print(
f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}"
Expand Down
14 changes: 14 additions & 0 deletions tests/test_term_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) 2025 Will Riley - original author
# Copyright (c) 2026 Jan Kowalleck - modifications and maintenance
# SPDX-License-Identifier: MIT

import pytest

import rfc3987_syntax2 as sut


@pytest.mark.parametrize("term", sut.RFC3987_SYNTAX_TERMS)
def test_term_validator_as_expected(term: str) -> None:
expected = getattr(sut, f'is_valid_syntax_{term}')
actual = sut.RFC3987_SYNTAX_TERM_VALIDATORS[term]
assert (actual is expected), f"unexpected func for {term}"