diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..ba1bb1a --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,21 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-lts-latest + tools: + python: "3.14" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt + +formats: all diff --git a/CHANGELOG.md b/CHANGELOG.md index 6396746..665f021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ All notable changes to this project will be documented in this file. * New dict `RFC3987_SYNTAX_TERM_VALIDATORS` mapping `RFC3987_SYNTAX_TERMS` to validator function (via [#49]) * Performance * Public symbols use lazy loading (via [#43], [#59]) +* Docs: + * Added docstrings to public API (via [#58]) * Tests * Add grammar regression tests (via [#48]) @@ -36,6 +38,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 +[#58]: https://github.com/jkowalleck/rfc3987-syntax2/pull/58 [#59]: https://github.com/jkowalleck/rfc3987-syntax2/pull/59 ## v1.2.0 - 2026-07-16 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a485625 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +/_build diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..0850117 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,54 @@ +import tomllib +from pathlib import Path +from datetime import datetime + + +_ROOT = Path(__file__).resolve().parents[1] +_pyproject = tomllib.loads((_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + +project = _pyproject["project"]["name"] +author = ", ".join(a["name"] for a in _pyproject["project"].get("authors", [])) or "Unknown" +copyright = f"{datetime.now().year}, {author}" + +release = _pyproject["project"].get("version", "-dev") + + +extensions = [ + "sphinx.ext.autodoc", + # "sphinx.ext.viewcode", + "autoapi.extension", + "sphinx_rtd_theme", + "myst_parser", +] + +source_suffix = ['.rst', '.md'] + +autoapi_type = 'python' +autoapi_dirs = ['../src/rfc3987_syntax2'] +# see https://sphinx-autoapi.readthedocs.io/en/latest/reference/config.html#confval-autoapi_options +autoapi_options = [ + 'show-module-summary', + 'members', + 'undoc-members', + 'inherited-members', + 'show-inheritance', + 'imported-members' +] + + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +# see https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-exclude_patterns +exclude_patterns = [ + '_dev', # internal docs + '_build', # build target + '.*', '**/.*', # dotfiles and folders + 'Thumbs.db', '**/Thumbs.db', +] + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..15332f0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,9 @@ +.. include:: ../README.md + :parser: myst_parser.sphinx_ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :titlesonly: + + autoapi/index diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..2985c12 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,6 @@ + + +myst-parser>=5.1.0,<6 +sphinx>=9,<10 +sphinx-autoapi>=3,<4 +sphinx-rtd-theme>=3,<4 diff --git a/src/rfc3987_syntax2/__init__.py b/src/rfc3987_syntax2/__init__.py index 3de7209..cb9cb7e 100644 --- a/src/rfc3987_syntax2/__init__.py +++ b/src/rfc3987_syntax2/__init__.py @@ -2,7 +2,11 @@ # Copyright (c) 2026 Jan Kowalleck - modifications and maintenance # SPDX-License-Identifier: MIT -from typing import Any +""" +Re-export of :mod:`.syntax_helpers` +""" + +from typing import Any, TYPE_CHECKING from . import syntax_helpers as _syntax_helpers @@ -10,6 +14,9 @@ _EXPORTS_SH = frozenset(_syntax_helpers.__all__) +if TYPE_CHECKING: + from .syntax_helpers import * +# cannot simply re-export - need to respect lazy-loading def __getattr__(name: str) -> Any: if name in _EXPORTS_SH: diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index 9b82840..3952bf3 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -17,8 +17,10 @@ "RFC3987_SYNTAX_GRAMMAR_PATH", "RFC3987_SYNTAX_TERMS", "grammar", - "syntax_parser", + "T_SYNTAX_PARSER_TERM", "parse", + "syntax_parser", + "T_SYNTAX_VALIDATOR", "is_valid_syntax", "make_syntax_validator", "RFC3987_SYNTAX_TERM_VALIDATORS", @@ -103,6 +105,7 @@ "ipv4address", "dec_octet", "digit", + # non_zero - a local helper token used in `dec_octet` "non_zero", "unreserved", "alpha", @@ -110,15 +113,47 @@ "port", "pct_encoded", ] +"""All supported RFC 3987 grammar rule names exposed by this module. + +These term names can be used to select validators from +:data:`RFC3987_SYNTAX_TERM_VALIDATORS` and correspond to rules +defined in the RFC 3987 Lark grammar. +""" + +T_SYNTAX_PARSER_TERM = Literal["iri", "iri_reference", "absolute_iri"] +"""Top-level RFC 3987 parser start terms accepted by :func:`parse` and :func:`check`. -_T_SYNTAX_PARSER_TERM = Literal["iri", "iri_reference", "absolute_iri"] -_SYNTAX_PARSER_STARTS: list[_T_SYNTAX_PARSER_TERM] = ["iri", "iri_reference", "absolute_iri"] +Allowed values are ``"iri"``, ``"iri_reference"``, and ``"absolute_iri"``. +""" +_SYNTAX_PARSER_STARTS: list[T_SYNTAX_PARSER_TERM] = ["iri", "iri_reference", "absolute_iri"] + +def parse(term: T_SYNTAX_PARSER_TERM, value: str) -> ParseTree: + """Parse text as one of the top-level RFC 3987 syntax terms. + + :param term: Start rule used for parsing. Must be one of ``"iri"``, + ``"iri_reference"``, or ``"absolute_iri"``. + :param value: Input text to parse. + :return: The Lark parse tree for ``value`` under the selected start rule. + :raises lark.exceptions.LarkError: If parser initialization fails or parsing + cannot be completed. + """ -def parse(term: _T_SYNTAX_PARSER_TERM, value: str) -> 'ParseTree': return _get_syntax_parser().parse(value, start=term) -def is_valid_syntax(term: _T_SYNTAX_PARSER_TERM, value: str) -> bool: +def is_valid_syntax(term: T_SYNTAX_PARSER_TERM, value: str) -> bool: + """Check whether text is valid for a top-level RFC 3987 syntax term. + + This is a boolean convenience wrapper around :func:`parse`. + + .. warning:: Emits a ``RuntimeWarning`` (via :func:`warnings.warn`) when a non-``UnexpectedInput`` ``lark.exceptions.LarkError`` occurs. + + :param term: Start rule used for validation. Must be one of ``"iri"``, + ``"iri_reference"``, or ``"absolute_iri"``. + :param value: Input text to validate. + :return: ``True`` if parsing succeeds; otherwise ``False``. + """ + try: parse(term=term, value=value) except exceptions.UnexpectedInput: @@ -133,9 +168,27 @@ def is_valid_syntax(term: _T_SYNTAX_PARSER_TERM, value: str) -> bool: return False return True -_T_SYNTAX_VALIDATOR = Callable[[str], bool] +T_SYNTAX_VALIDATOR = Callable[[str], bool] +"""Callable validator for one RFC 3987 grammar rule. + +:param text: Input text to validate. +:return: ``True`` if ``text`` matches the target rule, otherwise ``False``. +""" + + +def make_syntax_validator(rule_name: str) -> T_SYNTAX_VALIDATOR: + """Create a validator function for a specific RFC 3987 grammar rule. + + The returned callable lazily initializes and caches a dedicated + :class:`lark.Lark` parser configured with ``start=rule_name`` and + ``parser=RFC3987_SYNTAX_PARSER_TYPE``. + + :param rule_name: Grammar rule name to validate against. Must be a value from + :data:`RFC3987_SYNTAX_TERMS`. + :return: A callable ``(text: str) -> bool`` that returns ``True`` when parsing + succeeds and ``False`` if a ``lark.exceptions.LarkError`` is raised. + """ -def make_syntax_validator(rule_name: str) -> _T_SYNTAX_VALIDATOR: parser: Optional[Lark] = None parser_lock = Lock() @@ -161,6 +214,7 @@ def syntax_validator(text: str) -> bool: def _get_grammar() -> str: """this is private API""" + global _grammar with _grammar_lock: if _grammar is None: @@ -173,6 +227,7 @@ def _get_grammar() -> str: def _get_syntax_parser() -> Lark: """this is private API""" + global _syntax_parser if _syntax_parser is not None: return _syntax_parser @@ -184,90 +239,303 @@ def _get_syntax_parser() -> Lark: parser=RFC3987_SYNTAX_PARSER_TYPE) return _syntax_parser - -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_scheme = make_syntax_validator("scheme") - -is_valid_syntax_ihier_part = make_syntax_validator("ihier_part") - -is_valid_syntax_irelative_ref = make_syntax_validator("irelative_ref") - -is_valid_syntax_irelative_part = make_syntax_validator("irelative_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_digit = make_syntax_validator("digit") - -is_valid_syntax_non_zero = make_syntax_validator("non_zero") - -is_valid_syntax_unreserved = make_syntax_validator("unreserved") - -is_valid_syntax_alpha = make_syntax_validator("alpha") - -is_valid_syntax_hexdig = make_syntax_validator("hexdig") - -is_valid_syntax_port = make_syntax_validator("port") - -is_valid_syntax_pct_encoded = make_syntax_validator("pct_encoded") - -RFC3987_SYNTAX_TERM_VALIDATORS: dict[str, _T_SYNTAX_VALIDATOR] = { # frozendict +if TYPE_CHECKING: + def is_valid_syntax_iri(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``IRI`` rule.""" + ... +else: + is_valid_syntax_iri = make_syntax_validator("iri") + + +if TYPE_CHECKING: + def is_valid_syntax_iri_reference(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``IRI-reference`` rule.""" + ... +else: + is_valid_syntax_iri_reference = make_syntax_validator("iri_reference") + +if TYPE_CHECKING: + def is_valid_syntax_absolute_iri(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``absolute-IRI`` rule.""" + ... +else: + is_valid_syntax_absolute_iri = make_syntax_validator("absolute_iri") + +if TYPE_CHECKING: + def is_valid_syntax_scheme(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``scheme`` rule.""" + ... +else: + is_valid_syntax_scheme = make_syntax_validator("scheme") + +if TYPE_CHECKING: + def is_valid_syntax_ihier_part(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ihier-part`` rule.""" + ... +else: + is_valid_syntax_ihier_part = make_syntax_validator("ihier_part") + +if TYPE_CHECKING: + def is_valid_syntax_irelative_ref(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``irelative-ref`` rule.""" + ... +else: + is_valid_syntax_irelative_ref = make_syntax_validator("irelative_ref") + +if TYPE_CHECKING: + def is_valid_syntax_irelative_part(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``irelative-part`` rule.""" + ... +else: + is_valid_syntax_irelative_part = make_syntax_validator("irelative_part") + +if TYPE_CHECKING: + def is_valid_syntax_iauthority(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``iauthority`` rule.""" + ... +else: + is_valid_syntax_iauthority = make_syntax_validator("iauthority") + +if TYPE_CHECKING: + def is_valid_syntax_iuserinfo(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``iuserinfo`` rule.""" + ... +else: + is_valid_syntax_iuserinfo = make_syntax_validator("iuserinfo") + +if TYPE_CHECKING: + def is_valid_syntax_ihost(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ihost`` rule.""" + ... +else: + is_valid_syntax_ihost = make_syntax_validator("ihost") + +if TYPE_CHECKING: + def is_valid_syntax_ireg_name(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ireg-name`` rule.""" + ... +else: + is_valid_syntax_ireg_name = make_syntax_validator("ireg_name") + +if TYPE_CHECKING: + def is_valid_syntax_ipath(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ipath`` rule.""" + ... +else: + is_valid_syntax_ipath = make_syntax_validator("ipath") + +if TYPE_CHECKING: + def is_valid_syntax_ipath_abempty(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ipath-abempty`` rule.""" + ... +else: + is_valid_syntax_ipath_abempty = make_syntax_validator("ipath_abempty") + +if TYPE_CHECKING: + def is_valid_syntax_ipath_absolute(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ipath-absolute`` rule.""" + ... +else: + is_valid_syntax_ipath_absolute = make_syntax_validator("ipath_absolute") + +if TYPE_CHECKING: + def is_valid_syntax_ipath_noscheme(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ipath-noscheme`` rule.""" + ... +else: + is_valid_syntax_ipath_noscheme = make_syntax_validator("ipath_noscheme") + +if TYPE_CHECKING: + def is_valid_syntax_ipath_rootless(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ipath-rootless`` rule.""" + ... +else: + is_valid_syntax_ipath_rootless = make_syntax_validator("ipath_rootless") + +if TYPE_CHECKING: + def is_valid_syntax_ipath_empty(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ipath-empty`` rule.""" + ... +else: + is_valid_syntax_ipath_empty = make_syntax_validator("ipath_empty") + + +if TYPE_CHECKING: + def is_valid_syntax_isegment(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``isegment`` rule.""" + ... +else: + is_valid_syntax_isegment = make_syntax_validator("isegment") + +if TYPE_CHECKING: + def is_valid_syntax_isegment_nz(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``isegment-nz`` rule.""" + ... +else: + is_valid_syntax_isegment_nz = make_syntax_validator("isegment_nz") + +if TYPE_CHECKING: + def is_valid_syntax_isegment_nz_nc(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``isegment-nz-nc`` rule.""" + ... +else: + is_valid_syntax_isegment_nz_nc = make_syntax_validator("isegment_nz_nc") + +if TYPE_CHECKING: + def is_valid_syntax_ipchar(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ipchar`` rule.""" + ... +else: + is_valid_syntax_ipchar = make_syntax_validator("ipchar") + +if TYPE_CHECKING: + def is_valid_syntax_iquery(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``iquery`` rule.""" + ... +else: + is_valid_syntax_iquery = make_syntax_validator("iquery") + +if TYPE_CHECKING: + def is_valid_syntax_ifragment(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ifragment`` rule.""" + ... +else: + is_valid_syntax_ifragment = make_syntax_validator("ifragment") + +if TYPE_CHECKING: + def is_valid_syntax_iunreserved(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``iunreserved`` rule.""" + ... +else: + is_valid_syntax_iunreserved = make_syntax_validator("iunreserved") + +if TYPE_CHECKING: + def is_valid_syntax_ucschar(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ucschar`` rule.""" + ... +else: + is_valid_syntax_ucschar = make_syntax_validator("ucschar") + +if TYPE_CHECKING: + def is_valid_syntax_iprivate(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``iprivate`` rule.""" + ... +else: + is_valid_syntax_iprivate = make_syntax_validator("iprivate") + +if TYPE_CHECKING: + def is_valid_syntax_sub_delims(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``sub-delims`` rule.""" + ... +else: + is_valid_syntax_sub_delims = make_syntax_validator("sub_delims") + +if TYPE_CHECKING: + def is_valid_syntax_ip_literal(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``IP-literal`` rule.""" + ... +else: + is_valid_syntax_ip_literal = make_syntax_validator("ip_literal") + +if TYPE_CHECKING: + def is_valid_syntax_ipvfuture(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``IPvFuture`` rule.""" + ... +else: + is_valid_syntax_ipvfuture = make_syntax_validator("ipvfuture") + +if TYPE_CHECKING: + def is_valid_syntax_ipv6address(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``IPv6address`` rule.""" + ... +else: + is_valid_syntax_ipv6address = make_syntax_validator("ipv6address") + +if TYPE_CHECKING: + def is_valid_syntax_h16(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``h16`` rule.""" + ... +else: + is_valid_syntax_h16 = make_syntax_validator("h16") + +if TYPE_CHECKING: + def is_valid_syntax_ls32(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ls32`` rule.""" + ... +else: + is_valid_syntax_ls32 = make_syntax_validator("ls32") + +if TYPE_CHECKING: + def is_valid_syntax_ipv4address(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``IPv4address`` rule.""" + ... +else: + is_valid_syntax_ipv4address = make_syntax_validator("ipv4address") + +if TYPE_CHECKING: + def is_valid_syntax_dec_octet(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``dec-octet`` rule.""" + ... +else: + is_valid_syntax_dec_octet = make_syntax_validator("dec_octet") + +if TYPE_CHECKING: + def is_valid_syntax_digit(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``DIGIT`` rule.""" + ... +else: + is_valid_syntax_digit = make_syntax_validator("digit") + +if TYPE_CHECKING: + def is_valid_syntax_non_zero(text: str) -> bool: + """ + Local helper token used in ``dec_octet``. + + .. warning:: + Deprecated: this is not a stable/public entry point and may be removed + in a future release. + """ + ... +else: + is_valid_syntax_non_zero = make_syntax_validator("non_zero") + +if TYPE_CHECKING: + def is_valid_syntax_unreserved(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``unreserved`` rule.""" + ... +else: + is_valid_syntax_unreserved = make_syntax_validator("unreserved") + +if TYPE_CHECKING: + def is_valid_syntax_alpha(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``ALPHA`` rule.""" + ... +else: + is_valid_syntax_alpha = make_syntax_validator("alpha") + +if TYPE_CHECKING: + def is_valid_syntax_hexdig(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``HEXDIG`` rule.""" + ... +else: + is_valid_syntax_hexdig = make_syntax_validator("hexdig") + +if TYPE_CHECKING: + def is_valid_syntax_port(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``port`` rule.""" + ... +else: + is_valid_syntax_port = make_syntax_validator("port") + +if TYPE_CHECKING: + def is_valid_syntax_pct_encoded(text: str) -> bool: + """Validate that input text conforms to the RFC 3987 ``pct-encoded`` rule.""" + ... +else: + 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, @@ -310,13 +578,20 @@ def _get_syntax_parser() -> Lark: "port": is_valid_syntax_port, "pct_encoded": is_valid_syntax_pct_encoded, } +"""Mapping from syntax term to validator. +Allowed keys are the RFC3987 term literals (see :data:`RFC3987_SYNTAX_TERMS`). +""" # region lazy loaded attrs if TYPE_CHECKING: # types for lazy-loaded symbols grammar: str + """Lark grammar text for RFC 3987.""" + +if TYPE_CHECKING: # types for lazy-loaded symbols syntax_parser: Lark + """Lazily initialized parser for RFC 3987 syntax.""" def __getattr__(name: str) -> Any: diff --git a/src/rfc3987_syntax2/syntax_rfc3987.lark b/src/rfc3987_syntax2/syntax_rfc3987.lark index 1c30753..e3fab8d 100644 --- a/src/rfc3987_syntax2/syntax_rfc3987.lark +++ b/src/rfc3987_syntax2/syntax_rfc3987.lark @@ -2,6 +2,9 @@ // Copyright (c) 2026 Jan Kowalleck - modifications and maintenance // SPDX-License-Identifier: MIT +// Lark grammar based on RFC 3987's ABNF: +// https://www.rfc-editor.org/info/rfc3987/#section-2.2 + iri: scheme ":" ihier_part ("?" iquery)? ("#" ifragment)? iri_reference: iri | irelative_ref @@ -98,6 +101,7 @@ dec_octet: digit digit: T_DIGIT T_DIGIT: /[0-9]/ +// Local helper token used in `dec_octet` non_zero: T_NON_ZERO T_NON_ZERO: /[1-9]/