From 22a9705eab037d71d30649db7ef9410368471e46 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 24 Jul 2025 18:53:17 +0200 Subject: [PATCH 01/25] fix: grammar according to RFC3987 Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax/syntax_rfc3987.lark | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/rfc3987_syntax/syntax_rfc3987.lark b/src/rfc3987_syntax/syntax_rfc3987.lark index c1973c0..c69be59 100644 --- a/src/rfc3987_syntax/syntax_rfc3987.lark +++ b/src/rfc3987_syntax/syntax_rfc3987.lark @@ -57,8 +57,8 @@ ifragment: (ipchar | "/" | "?")* iunreserved: alpha | digit | "-" | "." | "_" | "~" | ucschar -ucschar: /[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]/ -iprivate: /[\uE000-\uF8FF]/ +ucschar: /[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF\U00010000-\U0001FFFD\U00020000-\U0002FFFD\U00030000-\U0003FFFD\U00040000-\U0004FFFD\U00050000-\U0005FFFD\U00060000-\U0006FFFD\U00070000-\U0007FFFD\U00080000-\U0008FFFD\U00090000-\U0009FFFD\U000A0000-\U000AFFFD\U000B0000-\U000BFFFD\U000C0000-\U000CFFFD\U000D0000-\U000DFFFD\U000E0000-\U000EFFFD]/ +iprivate: /[\uE000-\uF8FF\U000F0000-\U000FFFFD\U00100000-\U0010FFFD]/ sub_delims: "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=" @@ -67,15 +67,7 @@ ip_literal: "[" (ipv6address | ipvfuture) "]" ipvfuture: "v" hexdig+ "." (unreserved | sub_delims | ":")+ -ipv6address: h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" ls32 - | "::" h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" ls32 - | h16 "::" h16 ":" h16 ":" h16 ":" h16 ":" ls32 - | h16 ":" h16 "::" h16 ":" h16 ":" h16 ":" ls32 - | h16 ":" h16 ":" h16 "::" h16 ":" h16 ":" ls32 - | h16 ":" h16 ":" h16 ":" h16 "::" h16 ":" ls32 - | h16 ":" h16 ":" h16 ":" h16 ":" h16 "::" ls32 - | h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" h16 "::" h16 - | h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" h16 ":" h16 "::" +ipv6address: "TODO" h16: hexdig | hexdig hexdig From 917adfe1c1b49339424e84f34968b75bae72a600 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 24 Jul 2025 18:57:55 +0200 Subject: [PATCH 02/25] fix: grammar according to RFC3987 Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax/syntax_rfc3987.lark | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/rfc3987_syntax/syntax_rfc3987.lark b/src/rfc3987_syntax/syntax_rfc3987.lark index c69be59..365cf06 100644 --- a/src/rfc3987_syntax/syntax_rfc3987.lark +++ b/src/rfc3987_syntax/syntax_rfc3987.lark @@ -67,7 +67,15 @@ ip_literal: "[" (ipv6address | ipvfuture) "]" ipvfuture: "v" hexdig+ "." (unreserved | sub_delims | ":")+ -ipv6address: "TODO" +ipv6address: ( h16 ":" )~6 ls32 + | "::" ( h16 ":" )~3 ls32 + | h16? "::" ( h16 ":" )~4 ls32 + | ( ( h16 ":" )? h16 )? "::" ( h16 ":" )~3 ls32 + | ( ( h16 ":" )~0..2 h16 )? "::" ( h16 ":" )~2 ls32 + | ( ( h16 ":" )~0..3 h16 )? "::" h16 ":" ls32 + | ( ( h16 ":" )~0..4 h16 )? "::" ls32 + | ( ( h16 ":" )~0..5 h16 )? "::" h16 + | ( ( h16 ":" )~0..6 h16 )? "::" h16: hexdig | hexdig hexdig From ac70a09a0c98a5c92c412af60aeca2ec2ac4c5de Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Fri, 15 Aug 2025 17:10:44 +0200 Subject: [PATCH 03/25] tests: additional tests for ucschar, iprivate, IPv6address Signed-off-by: Jan Kowalleck --- tests/valid_syntax.json | 52 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/valid_syntax.json b/tests/valid_syntax.json index 6f60090..e19b45b 100644 --- a/tests/valid_syntax.json +++ b/tests/valid_syntax.json @@ -108,10 +108,60 @@ "value": "пример/тест#часть2", "expect_lark": true, "reason": "" + }, + { + "value": "ucschar/\u00A0-\uD7FF/\uF900-\uFFCF/\uFDF0-\uFFEF", + "expect_lark": true, + "reason": "`ucschar` ranges: %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF" + }, + { + "value": "ucschar/\uD800\uDC00-\uD83F\uDFFD/\uD840\uDC00-\uD87F\uDFFD/\uD880\uDC00-\uD8BF\uDFFD", + "expect_lark": true, + "reason": "`ucschar` ranges: %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD" + }, + { + "value": "ucschar/\uD8C0\uDC00-\uD8FF\uDFFD/\uD900\uDC00-\uD93F\uDFFD/\uD940\uDC00-\uD97F\uDFFD", + "expect_lark": true, + "reason": "`ucschar` ranges: %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD" + }, + { + "value": "ucschar/\uD980\uDC00-\uD9BF\uDFFD/\uD9C0\uDC00-\uD9FF\uDFFD/\uDA00\uDC00-\uDA3F\uDFFD", + "expect_lark": true, + "reason": "`ucschar` ranges: %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD" + }, + { + "value": "ucschar/\uDA40\uDC00-\uDA7F\uDFFD/\uDA80\uDC00-\uDABF\uDFFD/\uDAC0\uDC00-\uDAFF\uDFFD", + "expect_lark": true, + "reason": "`ucschar` ranges: %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD" + }, + { + "value": "ucschar/\uDB00\uDC00-\uDB3F\uDFFD/\uDB40\uDC00-\uDB7F\uDFFD", + "expect_lark": true, + "reason": "`ucschar` ranges: %xD0000-DFFFD / %xE0000-EFFFD" + }, + { + "value": "iprivate?foo=\uE000-\uF8FF&bar=\uDB80\uDC00-\uDBBF\uDFFD&baz=\uDBC0\uDC00-\uDBFF\uDFFD", + "expect_lark": true, + "reason": "`iprivate` ranges: %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD" + }, + { + "value": "//[2345:0425:2CA1:0:0:0567:5673:23b5]/foo", + "expect_lark": true, + "reason": "`IPv6address` case: 6( h16 \":\" ) ls32" + }, + { + "value": "//[2345::BEEF]/foo", + "expect_lark": true, + "reason": "`IPv6address` case: [ *5( h16 \":\" ) h16 ] \"::\" h16" + }, + { + "value": "//[2041:0:140F::875B:131B]/foo", + "expect_lark": true, + "reason": "`IPv6address` case: [ *4( h16 \":\" ) h16 ] \"::\" ls32" } ], "reserved": [], "scheme": [], "pct_encoded": [], "ipv4address": [] -} \ No newline at end of file +} From 5416e51772e144201f5f62a564654450ae35fa28 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 14 Jul 2026 19:01:06 +0200 Subject: [PATCH 04/25] ci: add test pipeline (#3) Signed-off-by: Jan Kowalleck --- .github/workflows/python-publish.yml | 77 ++++++++++++++++++++------- .github/workflows/python-tests.yml | 79 ++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/python-tests.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index dbaedd4..ed74d3f 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -10,30 +10,67 @@ name: Upload Python Package on: release: - types: [published] + types: [ published ] + +env: + PYTHON_VERSION_DEFAULT: "3.14" permissions: contents: read jobs: - deploy: - + quick-test: + name: QuickTest + runs-on: ubuntu-latest + steps: + - name: Checkout + # see https://github.com/actions/checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Setup Python Environment + # see https://github.com/actions/setup-python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ env.PYTHON_VERSION_DEFAULT }} + architecture: 'x64' + - name: Validate Python Environment + shell: python + run: | + import sys + print('Python %s on %s in %s' % (sys.version, sys.platform, sys.getdefaultencoding())) + - name: Install env dependencies + run: python -m pip install --upgrade pip + - name: Install project + run: python -m pip install --user -e ".[testing]" + - name: Run Tests + run: python -m pytest tests + publish: + name: Publish + needs: + - quick-test runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + - name: Checkout + # see https://github.com/actions/checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Setup Python Environment + # see https://github.com/actions/setup-python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ env.PYTHON_VERSION_DEFAULT }} + architecture: 'x64' + - name: Install env dependencies + run: | + python -m pip install --upgrade pip + python -m pip install build + - name: Build package + run: python -m build + - name: Publish package + # see https://github.com/pypa/gh-action-pypi-publish + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 # v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 0000000..72d5a39 --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,79 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + + +name: Python CI + +on: + workflow_dispatch: + pull_request: + push: + tags: [ 'v*' ] # run again on release tags to have tools mark them + branches: [ 'main', 'next' ] + schedule: + # schedule daily tests, since some dependencies are not intended to be pinned + # this means: at 23:42 every day + - cron: '42 23 * * *' + +env: + PYTHON_VERSION_DEFAULT: "3.14" + +permissions: + contents: read + +jobs: + build-and-test: + name: Test (${{ matrix.os }} py${{ matrix.python-version }} ${{ matrix.toxenv-factors }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + python-version: + - "3.14" # highest supported + - "3.13" + - "3.12" + - "3.11" + - "3.10" + - "3.9" # lowest supported + exclude: + - os: macos-latest # macos-latest is incompatible with some PY versions + python-version: "3.10" + - os: macos-latest # macos-latest is incompatible with some PY versions + python-version: "3.9" + steps: + - name: Disabled Git auto EOL CRLF transforms + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - name: Checkout + # see https://github.com/actions/checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Setup Python Environment + # see https://github.com/actions/setup-python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + - name: Validate Python Environment + shell: python + run: | + import sys + print('Python %s on %s in %s' % (sys.version, sys.platform, sys.getdefaultencoding())) + - name: Install env dependencies + run: python -m pip install --upgrade pip + - name: Install project + run: pip install --user -e ".[testing]" + - name: Run Tests + run: python -m pytest tests \ No newline at end of file From bdcbb987fe2a11255149afb33753f1b2c044d273 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 14 Jul 2026 19:44:36 +0200 Subject: [PATCH 05/25] zizmor (#4) Signed-off-by: Jan Kowalleck --- .github/workflows/zizmor.yml | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..ba6c4f1 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,44 @@ +# Analyzes all GitHub Actions workflows for security issues using zizmor. +# docs: https://docs.zizmor.sh/ +name: Zizmor + +on: + push: + branches: ['master', 'main'] + pull_request: + branches: ['**'] + workflow_dispatch: + schedule: + - cron: '0 0 * * 6' + +permissions: {} + +concurrency: + group: '${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: true + +jobs: + zizmor: + name: Zizmor + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout + # see https://github.com/actions/checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Run zizmor 🌈 + # see https://github.com/zizmorcore/zizmor-action + uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 + with: + # advanced-security: false => emit findings as workflow-command annotations (::error file=…) rather than + # uploading a SARIF report to GitHub's Security tab. + # Uploading SARIF requires `security-events: write` and GitHub Advanced Security (GHAS), + # both of which are unnecessary here and would violate the least-privilege policy. + # The two modes are mutually exclusive: advanced-security must be false for + # annotations to take effect. + advanced-security: false + annotations: true \ No newline at end of file From c49af7619f2eef26678323cf7a80962d153e371e Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 14 Jul 2026 20:09:02 +0200 Subject: [PATCH 06/25] chore: prep fork publishing (#2) Signed-off-by: Jan Kowalleck --- .github/workflows/python-publish.yml | 7 +- CHANGELOG.md | 20 ++++++ CITATION.cff | 71 ------------------- LICENSE | 1 + README.md | 20 +++--- pyproject.toml | 36 ++++++---- .../__init__.py | 0 .../syntax_helpers.py | 2 +- .../syntax_rfc3987.lark | 0 .../utils.py | 0 tests/test_syntax.py | 9 +-- 11 files changed, 63 insertions(+), 103 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 CITATION.cff rename src/{rfc3987_syntax => rfc3987_syntax2}/__init__.py (100%) rename src/{rfc3987_syntax => rfc3987_syntax2}/syntax_helpers.py (98%) rename src/{rfc3987_syntax => rfc3987_syntax2}/syntax_rfc3987.lark (100%) rename src/{rfc3987_syntax => rfc3987_syntax2}/utils.py (100%) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index ed74d3f..b413f04 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -50,6 +50,8 @@ jobs: needs: - quick-test runs-on: ubuntu-latest + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - name: Checkout # see https://github.com/actions/checkout @@ -70,7 +72,4 @@ jobs: run: python -m build - name: Publish package # see https://github.com/pypa/gh-action-pypi-publish - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 # v1.4.2 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..92aec27 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## Unreleased + + + +## v1.1.0 - 2025-07-18 + +Original release: + +* Features + * Added add support for IRI-reference and absolute-IRI +* Bugs + * Fixed single quote sub-delimiter + +## v1.0.0 - 2025-05-18 + +Original release: \ No newline at end of file diff --git a/CITATION.cff b/CITATION.cff deleted file mode 100644 index f062222..0000000 --- a/CITATION.cff +++ /dev/null @@ -1,71 +0,0 @@ -cff-version: 1.2.0 -title: rfc3987-syntax -message: >- - If you use this software, please cite it using the - metadata from this file. -type: software -authors: - - given-names: Will - family-names: Riley - email: wanderingwill@gmail.com - orcid: "https://orcid.org/0000-0003-1822-6756" - - given-names: Jan - family-names: Kowalleck -repository-code: >- - https://github.com/willynilly/rfc3987-syntax -abstract: >- - Helper functions to syntactically validate strings according to RFC 3987 -keywords: - - RFC 3987 - - RFC3987 - - validator - - syntax - - parser -license: MIT -version: "1.1.0" -date-released: "2025-07-18" -references: - - title: "abnf-to-regexp" - type: software - version: "1.1.3" - license: MIT - authors: - - given-names: Marko - family-names: Ristin - email: marko@ristin.ch - orcid: "" - - given-names: Oliver Steensen-Bech - family-names: Haagh - email: oliver@dmc.international - orcid: "" - - given-names: Sebastian - family-names: Heppner - email: s.heppner@iat.rwth-aachen.de - orcid: "" - repository-code: https://github.com/aas-core-works/abnf-to-regexp - - title: "lark" - type: software - version: 1.2.2 - license: MIT - authors: - - family-names: Shinan - given-names: Erez - email: erezshin@gmail.com - repository-code: https://github.com/lark-parser/lark - - title: "Internationalized Resource Identifiers (IRIs)" - authors: - - family-names: Dürst - given-names: Martin - - family-names: Suignard - given-names: Michel - date-released: 2005-01-01 - doi: "10.17487/RFC3987" - url: "https://www.rfc-editor.org/info/rfc3987" - type: standard - - title: "ChatGPT" - authors: - - name: OpenAI - type: software - version: "GPT-4o" - url: "https://chat.openai.com/chat" - \ No newline at end of file diff --git a/LICENSE b/LICENSE index dbc72c3..6efa3e6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2025 Will Riley +Copyright (c) 2026 Jan Kowalleck — modifications and maintenance. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 2e4d2e2..4623b0e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ -# rfc3987-syntax +# rfc3987-syntax2 Helper functions to parse and validate the **syntax** of terms defined in **[RFC 3987](https://www.rfc-editor.org/info/rfc3987)** — the IETF standard for Internationalized Resource Identifiers (IRIs). +> [!NOTE] +> This is a maintained fork of . ## 🎯 Purpose -The goal of `rfc3987-syntax` is to provide a **lightweight, permissively licensed Python module** for validating that strings conform to the **ABNF grammar defined in RFC 3987**. These helpers are: +The goal of `rfc3987-syntax2` is to provide a **lightweight, permissively licensed Python module** for validating that strings conform to the **ABNF grammar defined in RFC 3987**. These helpers are: - ✅ Strictly aligned with the **syntax rules of RFC 3987** - ✅ Built using a **permissive MIT license** @@ -17,7 +19,7 @@ The goal of `rfc3987-syntax` is to provide a **lightweight, permissively license ## 📄 License, Attribution, and Citation -**`rfc3987-syntax`** is licensed under the [MIT License](LICENSE), which allows reuse in both open source and commercial software. +**`rfc3987-syntax2`** is licensed under the [MIT License](LICENSE), which allows reuse in both open source and commercial software. This project: @@ -46,7 +48,7 @@ ChatGPT 40 was used during the original development process. Errors may exist du ## 📦 Installation ```bash -pip install rfc3987-syntax +pip install rfc3987-syntax2 ``` ## 🛠 Usage @@ -54,7 +56,7 @@ pip install rfc3987-syntax ### List all supported "terms" (i.e., non-terminals and terminals within ABNF production rules) used to validate the syntax of an IRI according to RFC 3987 ```python -from rfc3987_syntax import RFC3987_SYNTAX_TERMS +from rfc3987_syntax2 import RFC3987_SYNTAX_TERMS print("Supported terms:") for term in RFC3987_SYNTAX_TERMS: @@ -64,7 +66,7 @@ for term in RFC3987_SYNTAX_TERMS: ### Syntactically validate a string using the general-purpose validator ```python -from rfc3987_syntax import is_valid_syntax +from rfc3987_syntax2 import is_valid_syntax if is_valid_syntax(term='iri', value='http://github.com'): print("✓ Valid IRI syntax") @@ -79,8 +81,8 @@ if not is_valid_syntax(term='iri_reference', value='bob'): ### Alternatively, use term-specific helpers to validate RFC 3987 syntax. ```python -from rfc3987_syntax import is_valid_syntax_iri -from rfc3987_syntax import is_valid_syntax_iri_reference +from rfc3987_syntax2 import is_valid_syntax_iri +from rfc3987_syntax2 import is_valid_syntax_iri_reference if is_valid_syntax_iri('http://github.com'): print("✓ Valid IRI syntax") @@ -95,7 +97,7 @@ if is_valid_syntax_iri_reference('bob'): ### Get the Lark parse tree for a syntax validation (useful for additional semantic validation) ```python -from rfc3987_syntax import parse +from rfc3987_syntax2 import parse ptree: ParseTree = parse(term="iri", value="http://github.com") diff --git a/pyproject.toml b/pyproject.toml index e0e1c16..da96c4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "rfc3987-syntax" -version = "1.1.0" +name = "rfc3987-syntax2" +version = "1.1.0rc2" description = "Helper functions to syntactically validate strings according to RFC 3987." readme = "README.md" requires-python = ">=3.9" @@ -12,21 +12,21 @@ license = "MIT" license-files = ["LICEN[CS]E*"] keywords = ["RFC 3987", "RFC3987", "validator", "syntax", "parser"] authors = [ - { name = "Will Riley", email = "wanderingwill@gmail.com" }, - { name = "Jan Kowalleck" } + { name = "Jan Kowalleck" }, + { name = "Will Riley", email = "wanderingwill@gmail.com" } ] classifiers = [ + # ALPHA - not all features of RFC 3987 are implemented, yet "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", - "License :: OSI Approved :: Apache Software License", + "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering", "Topic :: Software Development", "Topic :: Utilities" @@ -34,10 +34,10 @@ classifiers = [ dependencies = ["lark>=1.2.2"] [project.urls] -Homepage = "https://github.com/willynilly/rfc3987-syntax" -Documentation = "https://github.com/willynilly/rfc3987-syntax#readme" -Issues = "https://github.com/willynilly/rfc3987-syntax/issues" -Source = "https://github.com/willynilly/rfc3987-syntax" +Homepage = "https://github.com/jkowalleck/rfc3987-syntax2" +Documentation = "https://github.com/jkowalleck/rfc3987-syntax2#readme" +Issues = "https://github.com/jkowalleck/rfc3987-syntax2/issues" +Source = "https://github.com/jkowalleck/rfc3987-syntax2" [project.optional-dependencies] testing = [ @@ -45,9 +45,17 @@ testing = [ ] [tool.pytest.ini_options] -pythonpath = [ - "src" +pythonpath = ["src"] + +[tool.hatch.build.targets.sdist] +exclude = [ + "/.*", + "/_*", +] +include = [ + "/src", + "/tests" ] -[tool.hatch.build.targets.sdist.force-include] -"CITATION.cff" = "src/rfc3987_syntax/CITATION.cff" \ No newline at end of file +[tool.hatch.build.targets.wheel] +packages = ["src/rfc3987_syntax2"] diff --git a/src/rfc3987_syntax/__init__.py b/src/rfc3987_syntax2/__init__.py similarity index 100% rename from src/rfc3987_syntax/__init__.py rename to src/rfc3987_syntax2/__init__.py diff --git a/src/rfc3987_syntax/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py similarity index 98% rename from src/rfc3987_syntax/syntax_helpers.py rename to src/rfc3987_syntax2/syntax_helpers.py index fcb3f32..117a2ce 100644 --- a/src/rfc3987_syntax/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -2,7 +2,7 @@ from pathlib import Path -from rfc3987_syntax.utils import load_grammar +from .utils import load_grammar RFC3987_SYNTAX_PARSER_TYPE: str = "earley" RFC3987_SYNTAX_GRAMMAR_PATH: Path = Path(__file__).parent / "syntax_rfc3987.lark" diff --git a/src/rfc3987_syntax/syntax_rfc3987.lark b/src/rfc3987_syntax2/syntax_rfc3987.lark similarity index 100% rename from src/rfc3987_syntax/syntax_rfc3987.lark rename to src/rfc3987_syntax2/syntax_rfc3987.lark diff --git a/src/rfc3987_syntax/utils.py b/src/rfc3987_syntax2/utils.py similarity index 100% rename from src/rfc3987_syntax/utils.py rename to src/rfc3987_syntax2/utils.py diff --git a/tests/test_syntax.py b/tests/test_syntax.py index d71923c..4974ae2 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -1,8 +1,9 @@ from pathlib import Path -import rfc3987_syntax as h import json import pytest +import rfc3987_syntax2 as sut + @pytest.fixture def valid_syntax_data(): @@ -19,13 +20,13 @@ def invalid_syntax_data(): def test_is_valid_syntax(valid_syntax_data): for term, valid_examples in valid_syntax_data.items(): for valid_example in valid_examples: - actual = h.is_valid_syntax(term=term, value=valid_example["value"]) + actual = sut.is_valid_syntax(term=term, value=valid_example["value"]) print("") print( f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" ) assert ( - actual is True + actual is True ), f"Failed term: {term} for '{valid_example['value']}' : {valid_example['reason']}" assert True @@ -33,7 +34,7 @@ def test_is_valid_syntax(valid_syntax_data): def test_not_is_valid_syntax(invalid_syntax_data): for term, invalid_examples in invalid_syntax_data.items(): for invalid_example in invalid_examples: - actual = h.is_valid_syntax(term=term, value=invalid_example["value"]) + actual = sut.is_valid_syntax(term=term, value=invalid_example["value"]) print("") print( f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}" From 8bc7a45cd6bf0b8a7eec7433036d17916e40cedb Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 14 Jul 2026 20:23:42 +0200 Subject: [PATCH 07/25] chore: prep v1.1.1 (#5) Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 11 +++++++++++ pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92aec27..3964458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file. +## v1.1.1 - 2026-07-14 + +### Fork release `rfc3987-syntax2` +- Forked from [`willynilly/rfc3987-syntax`](https://github.com/willynilly/rfc3987-syntax). +- Published this project as an independently maintained package under the new distribution name `rfc3987-syntax2`. +- Preserved upstream functionality as the baseline for this initial fork release. + +### Notes for `rfc3987-syntax2` +- This is the first release of the fork. +- No intentional behavioral changes from upstream are introduced in this release unless explicitly listed above. + ## v1.1.0 - 2025-07-18 Original release: diff --git a/pyproject.toml b/pyproject.toml index da96c4f..d810a91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "rfc3987-syntax2" -version = "1.1.0rc2" +version = "1.1.1" description = "Helper functions to syntactically validate strings according to RFC 3987." readme = "README.md" requires-python = ">=3.9" From 7895fe3e4c6f4ef78fe3d9d7e12ebe23346f5d14 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 09:47:43 +0200 Subject: [PATCH 08/25] docs: readme (#8) Signed-off-by: Jan Kowalleck --- README.md | 64 +++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 4623b0e..465ae59 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ ChatGPT 40 was used during the original development process. Errors may exist du ## 📦 Installation -```bash +```sh pip install rfc3987-syntax2 ``` @@ -108,11 +108,11 @@ print(ptree) This grammar was derived from: -- **[RFC 3987 – Internationalized Resource Identifiers (IRIs)]** +- **RFC 3987 – Internationalized Resource Identifiers (IRIs)** → Defines IRI syntax and extensions to URI (e.g. Unicode characters, `ucschar`) → https://datatracker.ietf.org/doc/html/rfc3987 -- **[RFC 3986 – Uniform Resource Identifier (URI): Generic Syntax)]** +- **RFC 3986 – Uniform Resource Identifier (URI): Generic Syntax)** → Provides reusable components like `scheme`, `authority`, `ipv4address`, etc. → https://datatracker.ietf.org/doc/html/rfc3986 @@ -120,33 +120,33 @@ This grammar was derived from: ### Rule-to-Source Mapping -| Rule/Component | Source | Notes | -|----------------------|------------|-------| -| `iri` | RFC 3987 | Top-level IRI rule | -| `iri_reference` | RFC 3987 | Top-level IRI Reference rule | -| `absolute_iri` | RFC 3987 | Top-level Absolute IRI rule | -| `scheme` | RFC 3986 | Referenced by RFC 3987 §2.2 | -| `ihier_part` | RFC 3987 | IRI-specific hierarchy | -| `irelative_ref` | RFC 3987 | IRI-specific relative ref | -| `irelative_part` | RFC 3987 | IRI-specific relative part | -| `iauthority` | RFC 3986 | Standard URI authority | -| `ipath_abempty` | RFC 3986 | Path format variant | -| `ipath_absolute` | RFC 3986 | Absolute path | -| `ipath_noscheme` | RFC 3986 | Path disallowing scheme prefix | -| `ipath_rootless` | RFC 3986 | Used in non-scheme contexts | -| `iquery` | RFC 3987 | Query extension to URI | -| `ifragment` | RFC 3987 | Fragment extension to URI | -| `ipchar`, `isegment` | RFC 3986 | Path characters and segments | -| `isegment_nz_nc` | RFC 3987 | IRI-specific path constraint | -| `iunreserved` | RFC 3987 | Includes `ucschar` | -| `ucschar`, `iprivate`| RFC 3987 | Unicode support | -| `sub_delims` | RFC 3986 | Reserved characters | -| `ip_literal` | RFC 3986 | IPv6 or IPvFuture in `[]` | -| `ipv6address` | RFC 3986 | Expanded forms only | -| `ipvfuture` | RFC 3986 | Forward-compatible | -| `ipv4address` | RFC 3986 | Dotted-decimal IPv4 | -| `ls32` | RFC 3986 | Final 32 bits of IPv6 | -| `h16`, `dec_octet` | RFC 3986 | Hex and decimal chunks | -| `port` | RFC 3986 | Optional numeric | -| `pct_encoded` | RFC 3986 | Percent encoding (e.g. `%20`) | +| Rule/Component | Source | Notes | +|----------------|--------|-------| +| `iri` | RFC 3987 | Top-level IRI rule | +| `iri_reference` | RFC 3987 | Top-level IRI Reference rule | +| `absolute_iri` | RFC 3987 | Top-level Absolute IRI rule | +| `scheme` | RFC 3986 | Referenced by RFC 3987 §2.2 | +| `ihier_part` | RFC 3987 | IRI-specific hierarchy | +| `irelative_ref` | RFC 3987 | IRI-specific relative ref | +| `irelative_part` | RFC 3987 | IRI-specific relative part | +| `iauthority` | RFC 3986 | Standard URI authority | +| `ipath_abempty` | RFC 3986 | Path format variant | +| `ipath_absolute` | RFC 3986 | Absolute path | +| `ipath_noscheme` | RFC 3986 | Path disallowing scheme prefix | +| `ipath_rootless` | RFC 3986 | Used in non-scheme contexts | +| `iquery` | RFC 3987 | Query extension to URI | +| `ifragment` | RFC 3987 | Fragment extension to URI | +| `ipchar`, `isegment` | RFC 3986 | Path characters and segments | +| `isegment_nz_nc` | RFC 3987 | IRI-specific path constraint | +| `iunreserved` | RFC 3987 | Includes `ucschar` | +| `ucschar`, `iprivate`| RFC 3987 | Unicode support | +| `sub_delims` | RFC 3986 | Reserved characters | +| `ip_literal` | RFC 3986 | IPv6 or IPvFuture in `[]` | +| `ipv6address` | RFC 3986 | Expanded forms only | +| `ipvfuture` | RFC 3986 | Forward-compatible | +| `ipv4address` | RFC 3986 | Dotted-decimal IPv4 | +| `ls32` | RFC 3986 | Final 32 bits of IPv6 | +| `h16`, `dec_octet` | RFC 3986 | Hex and decimal chunks | +| `port` | RFC 3986 | Optional numeric | +| `pct_encoded` | RFC 3986 | Percent encoding (e.g. `%20`) | | `alpha`, `digit`, `hexdig` | RFC 3986 | Character classes | From a2a5bd5aa4ddcecc3b320310596234c8f8e65d3a Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 10:17:13 +0200 Subject: [PATCH 09/25] chore: add codeowners (#11) Signed-off-by: Jan Kowalleck --- .github/CODEOWNERS | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d667282 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# see https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + + +# all maintainers are default-reviewers of new pull requests. +* @jkowalleck \ No newline at end of file From 9eebff5230429e41ecf5ac77a537146c6d92e9d1 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 10:31:17 +0200 Subject: [PATCH 10/25] docs: readme shields (#12) Signed-off-by: Jan Kowalleck --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 465ae59..78652e8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # rfc3987-syntax2 +[![shield_pypi-version]][link_pypi] +[![shield_gh-workflow-test]][link_gh-workflow-test] +[![shield_license]][license_file] + +---- + Helper functions to parse and validate the **syntax** of terms defined in **[RFC 3987](https://www.rfc-editor.org/info/rfc3987)** — the IETF standard for Internationalized Resource Identifiers (IRIs). > [!NOTE] @@ -150,3 +156,11 @@ This grammar was derived from: | `port` | RFC 3986 | Optional numeric | | `pct_encoded` | RFC 3986 | Percent encoding (e.g. `%20`) | | `alpha`, `digit`, `hexdig` | RFC 3986 | Character classes | + +[shield_gh-workflow-test]: https://img.shields.io/github/actions/workflow/status/jkowalleck/rfc3987-syntax2/python-tests.yml?branch=main&logo=GitHub&logoColor=white "tests" +[shield_pypi-version]: https://img.shields.io/pypi/v/rfc3987-syntax2?logo=Python&logoColor=white&label=PyPI "PyPI" +[shield_license]: https://img.shields.io/github/license/jkowalleck/rfc3987-syntax2?logo=open%20source%20initiative&logoColor=white "license" + +[link_pypi]: https://pypi.org/project/rfc3987-syntax2/ +[link_gh-workflow-test]: https://github.com/jkowalleck/rfc3987-syntax2/actions/workflows/python-tests.yml?query=branch%3Amain +[license_file]: https://github.com/jkowalleck/rfc3987-syntax2/blob/main/LICENSE From fa0439b26b95504483e4d1d37474f7d772e456a9 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 10:42:33 +0200 Subject: [PATCH 11/25] tests Signed-off-by: Jan Kowalleck --- tests/valid_syntax.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/valid_syntax.json b/tests/valid_syntax.json index e19b45b..a6bba92 100644 --- a/tests/valid_syntax.json +++ b/tests/valid_syntax.json @@ -110,7 +110,7 @@ "reason": "" }, { - "value": "ucschar/\u00A0-\uD7FF/\uF900-\uFFCF/\uFDF0-\uFFEF", + "value": "ucschar/\u00A0-\uD7FF/\uF900-\uFDCF/\uFDF0-\uFFEF", "expect_lark": true, "reason": "`ucschar` ranges: %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF" }, From aec02374e0d7dec711bb22e6d300dc10837170ed Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 10:44:15 +0200 Subject: [PATCH 12/25] docs Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3964458..cb67e54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file. +* Tests + * Added more tests (via [#10]) + +[#10]: https://github.com/jkowalleck/rfc3987-syntax2/pull/10 + ## v1.1.1 - 2026-07-14 ### Fork release `rfc3987-syntax2` From e58a2e93fde4b597eeacf3e4508f84275c10e17b Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 10:45:54 +0200 Subject: [PATCH 13/25] lark Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_rfc3987.lark | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rfc3987_syntax2/syntax_rfc3987.lark b/src/rfc3987_syntax2/syntax_rfc3987.lark index 365cf06..f0811e9 100644 --- a/src/rfc3987_syntax2/syntax_rfc3987.lark +++ b/src/rfc3987_syntax2/syntax_rfc3987.lark @@ -68,7 +68,7 @@ ip_literal: "[" (ipv6address | ipvfuture) "]" ipvfuture: "v" hexdig+ "." (unreserved | sub_delims | ":")+ ipv6address: ( h16 ":" )~6 ls32 - | "::" ( h16 ":" )~3 ls32 + | "::" ( h16 ":" )~5 ls32 | h16? "::" ( h16 ":" )~4 ls32 | ( ( h16 ":" )? h16 )? "::" ( h16 ":" )~3 ls32 | ( ( h16 ":" )~0..2 h16 )? "::" ( h16 ":" )~2 ls32 From 9895f2c3de61eae2d8ea1315dd1deb8cb896137b Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 10:48:31 +0200 Subject: [PATCH 14/25] docs: remove cite (#13) Signed-off-by: Jan Kowalleck --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 78652e8..ae2a063 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,6 @@ This project: > ⚠️ This project is **not affiliated with or endorsed by** the authors of RFC 3987 or the `rfc3987` Python package. -Please cite this software in accordance with the enclosed CITATION.cff file. - - ## ⚠️ Limitations The grammar and parser enforce **only the ABNF syntax** defined in RFC 3987. The following are **not validated** and must be handled separately for full compliance: From 31b5a21507a633209c1cee3c361f6e4ae42cbe06 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 11:05:55 +0200 Subject: [PATCH 15/25] tests: parameterize (#7) Signed-off-by: Jan Kowalleck --- tests/test_syntax.py | 54 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/test_syntax.py b/tests/test_syntax.py index 4974ae2..6886530 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -5,40 +5,40 @@ import rfc3987_syntax2 as sut -@pytest.fixture def valid_syntax_data(): with open(Path("tests", "valid_syntax.json"), "r", encoding="utf-8") as f: return json.load(f) -@pytest.fixture def invalid_syntax_data(): with open(Path("tests", "invalid_syntax.json"), "r", encoding="utf-8") as f: return json.load(f) -def test_is_valid_syntax(valid_syntax_data): - for term, valid_examples in valid_syntax_data.items(): - for valid_example in valid_examples: - actual = sut.is_valid_syntax(term=term, value=valid_example["value"]) - print("") - print( - f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" - ) - assert ( - actual is True - ), f"Failed term: {term} for '{valid_example['value']}' : {valid_example['reason']}" - assert True - - -def test_not_is_valid_syntax(invalid_syntax_data): - for term, invalid_examples in invalid_syntax_data.items(): - for invalid_example in invalid_examples: - actual = sut.is_valid_syntax(term=term, value=invalid_example["value"]) - print("") - print( - f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}" - ) - assert ( - actual is False - ), f"Failed term: {term} for '{invalid_example['value']}' : {invalid_example['reason']}" +def syntax_data_as_params(src_cb): + for term, examples in src_cb().items(): + for example in examples: + yield pytest.param(term, example, id=f"{term}-{example['value']}") + + +@pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data)) +def test_is_valid_syntax(term, valid_example): + actual = sut.is_valid_syntax(term=term, value=valid_example["value"]) + print("") + print( + f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" + ) + assert ( + actual is True + ), f"Failed term: {term} for '{valid_example['value']}' : {valid_example['reason']}" + +@pytest.mark.parametrize("term,invalid_example", syntax_data_as_params(invalid_syntax_data)) +def test_not_is_valid_syntax(term, invalid_example): + actual = sut.is_valid_syntax(term=term, value=invalid_example["value"]) + print("") + print( + f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}" + ) + assert ( + actual is False + ), f"Failed term: {term} for '{invalid_example['value']}' : {invalid_example['reason']}" From 593e46cec4241db47fcc72a6174ad4f27669000a Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 11:10:36 +0200 Subject: [PATCH 16/25] tests: move test data (#14) Signed-off-by: Jan Kowalleck --- tests/__init__.py | 9 +++++++++ tests/{ => _data}/invalid_syntax.json | 0 tests/{ => _data}/valid_syntax.json | 0 tests/test_syntax.py | 5 +++-- 4 files changed, 12 insertions(+), 2 deletions(-) rename tests/{ => _data}/invalid_syntax.json (100%) rename tests/{ => _data}/valid_syntax.json (100%) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..f282358 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,9 @@ +from pathlib import Path + +TESTS_ROOT_DIR = Path(__file__).parent +TESTS_DATA_DIR = TESTS_ROOT_DIR / '_data' + +TESTS_DATA_FILES = { + 'invalid_syntax': TESTS_DATA_DIR / 'invalid_syntax.json', + 'valid_syntax': TESTS_DATA_DIR / 'valid_syntax.json', +} \ No newline at end of file diff --git a/tests/invalid_syntax.json b/tests/_data/invalid_syntax.json similarity index 100% rename from tests/invalid_syntax.json rename to tests/_data/invalid_syntax.json diff --git a/tests/valid_syntax.json b/tests/_data/valid_syntax.json similarity index 100% rename from tests/valid_syntax.json rename to tests/_data/valid_syntax.json diff --git a/tests/test_syntax.py b/tests/test_syntax.py index 6886530..df59181 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -4,14 +4,15 @@ import rfc3987_syntax2 as sut +from . import TESTS_DATA_FILES def valid_syntax_data(): - with open(Path("tests", "valid_syntax.json"), "r", encoding="utf-8") as f: + with open(TESTS_DATA_FILES['valid_syntax'], "r", encoding="utf-8") as f: return json.load(f) def invalid_syntax_data(): - with open(Path("tests", "invalid_syntax.json"), "r", encoding="utf-8") as f: + with open(TESTS_DATA_FILES['invalid_syntax'], "r", encoding="utf-8") as f: return json.load(f) From 4043ef247be87ad88606c9f1a3e9c92d8d013729 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 11:42:24 +0200 Subject: [PATCH 17/25] tests: typing (#15) Signed-off-by: Jan Kowalleck --- pyproject.toml | 1 + tests/__init__.py | 23 ++++++++++++++++++++++- tests/test_syntax.py | 23 ++++++++--------------- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d810a91..43251d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ Source = "https://github.com/jkowalleck/rfc3987-syntax2" [project.optional-dependencies] testing = [ + "typing_extensions>=4.16.0", "pytest>=8.3.5", ] diff --git a/tests/__init__.py b/tests/__init__.py index f282358..d6eb5d2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,7 @@ +import json from pathlib import Path +from typing import TypedDict +from typing_extensions import Required, NotRequired TESTS_ROOT_DIR = Path(__file__).parent TESTS_DATA_DIR = TESTS_ROOT_DIR / '_data' @@ -6,4 +9,22 @@ TESTS_DATA_FILES = { 'invalid_syntax': TESTS_DATA_DIR / 'invalid_syntax.json', 'valid_syntax': TESTS_DATA_DIR / 'valid_syntax.json', -} \ No newline at end of file +} + +class SyntaxCase(TypedDict): + value: Required[str] + reason: Required[str] + expect_lark: NotRequired[bool] + expect_regex: NotRequired[bool] + conforms_with_rfc3987_semantics: NotRequired[bool] + semantics_notes: NotRequired[str] + +T_syntax_file = dict[str, list[SyntaxCase]] + +def valid_syntax_data() -> T_syntax_file: + with open(TESTS_DATA_FILES['valid_syntax'], "r", encoding="utf-8") as f: + return json.load(f) + +def invalid_syntax_data() -> T_syntax_file: + with open(TESTS_DATA_FILES['invalid_syntax'], "r", encoding="utf-8") as f: + return json.load(f) \ No newline at end of file diff --git a/tests/test_syntax.py b/tests/test_syntax.py index df59181..e50a767 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -1,29 +1,21 @@ -from pathlib import Path -import json +from typing import TYPE_CHECKING + import pytest +from typing import Callable import rfc3987_syntax2 as sut -from . import TESTS_DATA_FILES - -def valid_syntax_data(): - with open(TESTS_DATA_FILES['valid_syntax'], "r", encoding="utf-8") as f: - return json.load(f) - +from . import valid_syntax_data, invalid_syntax_data, SyntaxCase, T_syntax_file -def invalid_syntax_data(): - with open(TESTS_DATA_FILES['invalid_syntax'], "r", encoding="utf-8") as f: - return json.load(f) - -def syntax_data_as_params(src_cb): +def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]): for term, examples in src_cb().items(): for example in examples: yield pytest.param(term, example, id=f"{term}-{example['value']}") @pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data)) -def test_is_valid_syntax(term, valid_example): +def test_is_valid_syntax(term: str, valid_example: SyntaxCase) -> None: actual = sut.is_valid_syntax(term=term, value=valid_example["value"]) print("") print( @@ -33,8 +25,9 @@ def test_is_valid_syntax(term, valid_example): actual is True ), f"Failed term: {term} for '{valid_example['value']}' : {valid_example['reason']}" + @pytest.mark.parametrize("term,invalid_example", syntax_data_as_params(invalid_syntax_data)) -def test_not_is_valid_syntax(term, invalid_example): +def test_not_is_valid_syntax(term: str, invalid_example: SyntaxCase) -> None: actual = sut.is_valid_syntax(term=term, value=invalid_example["value"]) print("") print( From 42d5986f3d2c426b75720fccaa2b6fb38c008e5d Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 12:35:55 +0200 Subject: [PATCH 18/25] chore: editorconfig (#17) Signed-off-by: Jan Kowalleck --- .editorconfig | 44 ++++++++++++++++++++++++++++++++++++ .github/workflows/zizmor.yml | 8 +++---- 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2d70100 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,44 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +end_of_line = lf + +[*.py] +indent_style = space +indent_size = 4 + +[*.lark] +indent_style = space + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 4 + +[*.toml] +indent_style = space +indent_size = 4 + +[*.md] +charset = latin1 +indent_style = space +indent_size = 2 +# 2 trailing spaces indicate line breaks. +trim_trailing_whitespace = false + +[*.{rst,txt}] +indent_style = space +indent_size = 4 + +[{*.ini,.bandit,.flake8}] +charset = latin1 +indent_style = space +indent_size = 4 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index ba6c4f1..727ebb1 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -4,14 +4,14 @@ name: Zizmor on: push: - branches: ['master', 'main'] + branches: [ 'master', 'main' ] pull_request: - branches: ['**'] + branches: [ '**' ] workflow_dispatch: schedule: - cron: '0 0 * * 6' -permissions: {} +permissions: { } concurrency: group: '${{ github.workflow }}-${{ github.ref }}' @@ -41,4 +41,4 @@ jobs: # The two modes are mutually exclusive: advanced-security must be false for # annotations to take effect. advanced-security: false - annotations: true \ No newline at end of file + annotations: true From d172ffb95f455b5ea39e62296510a44f302913dc Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 12:36:09 +0200 Subject: [PATCH 19/25] feat: typing (#16) Signed-off-by: Jan Kowalleck --- .github/workflows/python-tests.yml | 28 ++++++++++++++++++++++ .mypy.ini | 34 +++++++++++++++++++++++++++ CHANGELOG.md | 5 ++++ pyproject.toml | 4 +++- src/rfc3987_syntax2/py.typed | 1 + src/rfc3987_syntax2/syntax_helpers.py | 8 ++++--- src/rfc3987_syntax2/utils.py | 2 +- tests/__init__.py | 4 ++-- tests/test_syntax.py | 4 ++-- 9 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 .mypy.ini create mode 100644 src/rfc3987_syntax2/py.typed diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 72d5a39..e0262eb 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -27,6 +27,34 @@ permissions: contents: read jobs: + static-code-analysis: + name: StaticCodingAnalysis (py${{ matrix.python-version}} ${{ matrix.toxenv-factors }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + include: + - python-version: '3.14' # latest + os: ubuntu-latest + - python-version: '3.9' # lowest + os: ubuntu-latest + steps: + - name: Checkout + # see https://github.com/actions/checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Setup Python Environment + # see https://github.com/actions/setup-python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + - name: Install project + run: pip install --user -e ".[testing]" + - name: Run Mypy + run: python -m mypy -v build-and-test: name: Test (${{ matrix.os }} py${{ matrix.python-version }} ${{ matrix.toxenv-factors }}) runs-on: ${{ matrix.os }} diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 0000000..13fb917 --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,34 @@ +[mypy] + +files = src/ + +show_error_codes = True +pretty = True + +warn_unreachable = True +allow_redefinition = False + +# ignore_missing_imports = False +# follow_imports = normal +# follow_imports_for_stubs = True + +### Strict mode ### +warn_unused_configs = True +disallow_subclassing_any = True +disallow_any_generics = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +check_untyped_defs = True +disallow_untyped_decorators = True +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = True +warn_return_any = True +no_implicit_reexport = True + +[mypy-pytest.*] +ignore_missing_imports = True + +[mypy-tests.*] +disallow_untyped_decorators = False \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3964458..58fbe21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file. +* Added + * Assert static typing (via [#16]) + +[#16]: https://github.com/jkowalleck/rfc3987-syntax2/pull/16 + ## v1.1.1 - 2026-07-14 ### Fork release `rfc3987-syntax2` diff --git a/pyproject.toml b/pyproject.toml index 43251d8..cbcc0e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,8 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", "Topic :: Software Development", - "Topic :: Utilities" + "Topic :: Utilities", + "Typing :: Typed", ] dependencies = ["lark>=1.2.2"] @@ -41,6 +42,7 @@ Source = "https://github.com/jkowalleck/rfc3987-syntax2" [project.optional-dependencies] testing = [ + "mypy>=1.15,<2", "typing_extensions>=4.16.0", "pytest>=8.3.5", ] diff --git a/src/rfc3987_syntax2/py.typed b/src/rfc3987_syntax2/py.typed new file mode 100644 index 0000000..161a597 --- /dev/null +++ b/src/rfc3987_syntax2/py.typed @@ -0,0 +1 @@ +# see https://peps.python.org/pep-0561/ \ No newline at end of file diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index 117a2ce..2e3876e 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -1,3 +1,5 @@ +from typing import Callable + from lark import Lark, ParseTree, exceptions from pathlib import Path @@ -54,7 +56,7 @@ def parse(term: str, value: str) -> ParseTree: return syntax_parser.parse(value, start=term) -def is_valid_syntax(term: str, value: str): +def is_valid_syntax(term: str, value: str) -> bool: try: parse(term=term, value=value) return True @@ -62,10 +64,10 @@ def is_valid_syntax(term: str, value: str): return False -def make_syntax_validator(rule_name): +def make_syntax_validator(rule_name: str) -> Callable[[str], bool]: parser = Lark(grammar, start=rule_name, parser=RFC3987_SYNTAX_PARSER_TYPE) - def syntax_validator(text): + def syntax_validator(text: str) -> bool: try: parser.parse(text) return True diff --git a/src/rfc3987_syntax2/utils.py b/src/rfc3987_syntax2/utils.py index a310886..af16a5d 100644 --- a/src/rfc3987_syntax2/utils.py +++ b/src/rfc3987_syntax2/utils.py @@ -1,6 +1,6 @@ from pathlib import Path -def load_grammar(path: Path): +def load_grammar(path: Path) -> str: with open(path, "r", encoding="utf-8") as file: return file.read() diff --git a/tests/__init__.py b/tests/__init__.py index d6eb5d2..4585dbd 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -23,8 +23,8 @@ class SyntaxCase(TypedDict): def valid_syntax_data() -> T_syntax_file: with open(TESTS_DATA_FILES['valid_syntax'], "r", encoding="utf-8") as f: - return json.load(f) + return json.load(f) # type:ignore[no-any-return] def invalid_syntax_data() -> T_syntax_file: with open(TESTS_DATA_FILES['invalid_syntax'], "r", encoding="utf-8") as f: - return json.load(f) \ No newline at end of file + return json.load(f) # type:ignore[no-any-return] \ No newline at end of file diff --git a/tests/test_syntax.py b/tests/test_syntax.py index e50a767..4262b88 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -1,14 +1,14 @@ from typing import TYPE_CHECKING import pytest -from typing import Callable +from typing import Any, Callable import rfc3987_syntax2 as sut from . import valid_syntax_data, invalid_syntax_data, SyntaxCase, T_syntax_file -def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]): +def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]) -> Any: for term, examples in src_cb().items(): for example in examples: yield pytest.param(term, example, id=f"{term}-{example['value']}") From 0b0486d6a5543f82a4dba28479c71f375464a3f9 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 12:43:10 +0200 Subject: [PATCH 20/25] docs Signed-off-by: Jan Kowalleck --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ae2a063..2bd6b24 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,12 @@ Helper functions to parse and validate the **syntax** of terms defined in **[RFC The goal of `rfc3987-syntax2` is to provide a **lightweight, permissively licensed Python module** for validating that strings conform to the **ABNF grammar defined in RFC 3987**. These helpers are: -- ✅ Strictly aligned with the **syntax rules of RFC 3987** +- ✅ Strictly aligned with the **syntax rules of RFC 3987** (including grammar corrections reflected in current releases) - ✅ Built using a **permissive MIT license** - ✅ Designed for both **open source and proprietary use** - ✅ Powered by [Lark](https://github.com/lark-parser/lark), a fast, EBNF-based parser -> 🧠 **Note:** This project focuses on **syntax validation only**. RFC 3987 specifies **additional semantic rules** (e.g., Unicode normalization, BiDi constraints, percent-encoding requirements) that must be enforced separately. - +> 🧠 **Note:** This project focuses on **syntax validation only**. RFC 3987 specifies **additional semantic rules** (e.g., Unicode normalization, BiDi constraints, percent-encoding requirements) that are intentionally out of scope. ## 📄 License, Attribution, and Citation @@ -37,17 +36,16 @@ This project: ## ⚠️ Limitations -The grammar and parser enforce **only the ABNF syntax** defined in RFC 3987. The following are **not validated** and must be handled separately for full compliance: +The grammar and parser enforce **only the ABNF syntax** defined in RFC 3987 (and referenced RFC 3986 productions where applicable). The following are **not validated** and must be handled separately for full compliance: - ✅ Unicode **Normalization Form C (NFC)** - ✅ Bidirectional text (**BiDi**) constraints (RFC 3987 §4.1) - ✅ **Port number ranges** (must be 0–65535) -- ✅ Valid **IPv6 compression** (only one `::`, max segments) - ✅ Context-aware **percent-encoding** requirements +- ✅ Additional host/network semantics beyond ABNF syntax (for example, canonical representation expectations) ChatGPT 40 was used during the original development process. Errors may exist due to this assistance. Additional review, testing, and bug fixes by human experts is welcome. - ## 📦 Installation ```sh @@ -92,7 +90,7 @@ if is_valid_syntax_iri('http://github.com'): if not is_valid_syntax_iri('bob'): print("✗ Invalid IRI syntax") - + if is_valid_syntax_iri_reference('bob'): print("✓ Valid IRI-reference syntax") ``` @@ -115,7 +113,7 @@ This grammar was derived from: → Defines IRI syntax and extensions to URI (e.g. Unicode characters, `ucschar`) → https://datatracker.ietf.org/doc/html/rfc3987 -- **RFC 3986 – Uniform Resource Identifier (URI): Generic Syntax)** +- **RFC 3986 – Uniform Resource Identifier (URI): Generic Syntax** → Provides reusable components like `scheme`, `authority`, `ipv4address`, etc. → https://datatracker.ietf.org/doc/html/rfc3986 @@ -142,10 +140,10 @@ This grammar was derived from: | `ipchar`, `isegment` | RFC 3986 | Path characters and segments | | `isegment_nz_nc` | RFC 3987 | IRI-specific path constraint | | `iunreserved` | RFC 3987 | Includes `ucschar` | -| `ucschar`, `iprivate`| RFC 3987 | Unicode support | +| `ucschar`, `iprivate` | RFC 3987 | Unicode support | | `sub_delims` | RFC 3986 | Reserved characters | | `ip_literal` | RFC 3986 | IPv6 or IPvFuture in `[]` | -| `ipv6address` | RFC 3986 | Expanded forms only | +| `ipv6address` | RFC 3986 | RFC 3986 IPv6address production; syntax-level validation only | | `ipvfuture` | RFC 3986 | Forward-compatible | | `ipv4address` | RFC 3986 | Dotted-decimal IPv4 | | `ls32` | RFC 3986 | Final 32 bits of IPv6 | From f05b302ffb0038bb6c331507f083741e8c1dc6fa Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 12:49:08 +0200 Subject: [PATCH 21/25] docs Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d9f512..265e214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,17 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +* Fixed + * Align RFC 3987 grammar handling with the specification (via [#10]) + * Fix RFC 3986-delegated IRI parsing edge cases (via [#10]) +* Changed + * Update grammar rules used by `is_valid_syntax(...)` and term-specific validators (via [#10]) * Added * Assert static typing (via [#16]) * Tests - * Added more tests (via [#10]) + * Add grammar regression tests (via [#10]) [#10]: https://github.com/jkowalleck/rfc3987-syntax2/pull/10 [#16]: https://github.com/jkowalleck/rfc3987-syntax2/pull/16 From f829dc444bc25987da48c802d7005213d188d91b Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 12:52:14 +0200 Subject: [PATCH 22/25] docs: chaneglog streamlined (#18) Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58fbe21..36140a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,13 +14,13 @@ All notable changes to this project will be documented in this file. ## v1.1.1 - 2026-07-14 ### Fork release `rfc3987-syntax2` -- Forked from [`willynilly/rfc3987-syntax`](https://github.com/willynilly/rfc3987-syntax). -- Published this project as an independently maintained package under the new distribution name `rfc3987-syntax2`. -- Preserved upstream functionality as the baseline for this initial fork release. +* Forked from [`willynilly/rfc3987-syntax`](https://github.com/willynilly/rfc3987-syntax). +* Published this project as an independently maintained package under the new distribution name `rfc3987-syntax2`. +* Preserved upstream functionality as the baseline for this initial fork release. ### Notes for `rfc3987-syntax2` -- This is the first release of the fork. -- No intentional behavioral changes from upstream are introduced in this release unless explicitly listed above. +* This is the first release of the fork. +* No intentional behavioral changes from upstream are introduced in this release unless explicitly listed above. ## v1.1.0 - 2025-07-18 @@ -33,4 +33,4 @@ Original release: \ No newline at end of file +Original release: From 8e3e6f094451641583fa11549c4fd1331ff91c94 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 12:56:14 +0200 Subject: [PATCH 23/25] docs Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 265e214..7e6bab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,6 @@ All notable changes to this project will be documented in this file. -## [Unreleased] - * Fixed * Align RFC 3987 grammar handling with the specification (via [#10]) * Fix RFC 3986-delegated IRI parsing edge cases (via [#10]) From 105b24f1363b5e86d06ea4abb08d9b1aa82f9acc Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 13:02:03 +0200 Subject: [PATCH 24/25] Apply suggestions from code review Co-authored-by: Jan Kowalleck --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bd6b24..395762f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Helper functions to parse and validate the **syntax** of terms defined in **[RFC The goal of `rfc3987-syntax2` is to provide a **lightweight, permissively licensed Python module** for validating that strings conform to the **ABNF grammar defined in RFC 3987**. These helpers are: -- ✅ Strictly aligned with the **syntax rules of RFC 3987** (including grammar corrections reflected in current releases) +- ✅ Strictly aligned with the **syntax rules of RFC 3987** - ✅ Built using a **permissive MIT license** - ✅ Designed for both **open source and proprietary use** - ✅ Powered by [Lark](https://github.com/lark-parser/lark), a fast, EBNF-based parser From f661bd3019a0a28e31ab59baa1bcc4f37280b720 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 16 Jul 2026 13:04:02 +0200 Subject: [PATCH 25/25] docs Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffa37ce..bb58846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. * Changed * Update grammar rules used by `is_valid_syntax(...)` and term-specific validators (via [#10]) * Added + * Validate IPv6 compression (via [#10]) * Assert static typing (via [#16]) * Tests * Add grammar regression tests (via [#10])