diff --git a/CHANGELOG.md b/CHANGELOG.md index c122dc3..2eedfc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,19 @@ All notable changes to this project will be documented in this file. * Fix `RFC3987_SYNTAX_TERMS` correctly containing "ipath_rootless" (via [#41]) * Fix `RFC3987_SYNTAX_TERMS` correctly containing "ipath_empty" (via [#41]) * Fix `ucschar` range (via [#45]) +* Added + * Function `is_valid_syntax_scheme` (via [#48]) + * Function `is_valid_syntax_non_zero` (via [#48]) + * Function `is_valid_syntax_pct_encoded` (via [#48]) * Performance * Public symbols use lazy loading (via [#43]) +* Tests + * Add grammar regression tests (via [#48]) [#41]: https://github.com/jkowalleck/rfc3987-syntax2/pull/41 [#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 ## v1.2.0 - 2026-07-16 diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index ecdb943..bbf821f 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -22,9 +22,10 @@ "is_valid_syntax_iri", "is_valid_syntax_iri_reference", "is_valid_syntax_absolute_iri", + "is_valid_syntax_scheme", + "is_valid_syntax_ihier_part", "is_valid_syntax_irelative_ref", "is_valid_syntax_irelative_part", - "is_valid_syntax_ihier_part", "is_valid_syntax_iauthority", "is_valid_syntax_iuserinfo", "is_valid_syntax_ihost", @@ -52,11 +53,13 @@ "is_valid_syntax_ls32", "is_valid_syntax_ipv4address", "is_valid_syntax_dec_octet", + "is_valid_syntax_digit", + "is_valid_syntax_non_zero", "is_valid_syntax_unreserved", "is_valid_syntax_alpha", - "is_valid_syntax_digit", "is_valid_syntax_hexdig", "is_valid_syntax_port", + "is_valid_syntax_pct_encoded", ] @@ -173,12 +176,14 @@ def _get_syntax_parser() -> Lark: 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_ihier_part = make_syntax_validator("ihier_part") - is_valid_syntax_iauthority = make_syntax_validator("iauthority") is_valid_syntax_iuserinfo = make_syntax_validator("iuserinfo") @@ -233,16 +238,20 @@ def _get_syntax_parser() -> Lark: 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_digit = make_syntax_validator("digit") - 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") + # region lazy loaded attrs if TYPE_CHECKING: # types for lazy-loaded symbols diff --git a/tests/_data/.editorconfig b/tests/_data/.editorconfig new file mode 100644 index 0000000..8ffdbaf --- /dev/null +++ b/tests/_data/.editorconfig @@ -0,0 +1,10 @@ +# Compatibility: EditorConfig has no ASCII charset setting. +# Keep these JSON fixtures ASCII-only and represent non-ASCII code points +# with JSON Unicode escapes. + +[*.json] +charset = latin1 +indent_style = space +indent_size = 4 +insert_final_newline = true +end_of_line = lf diff --git a/tests/_data/.gitattributes b/tests/_data/.gitattributes new file mode 100644 index 0000000..2fad8de --- /dev/null +++ b/tests/_data/.gitattributes @@ -0,0 +1,2 @@ +# Compatibility: keep fixture JSON files opaque to Git's text handling. +*.json binary diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index ab48fa1..e17ad63 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -1,23 +1,23 @@ { "ucschar": [ { - "value": "\u001f", + "value": "\u001F", "reason": "Control character not allowed in UCS range" }, { - "value": "", + "value": "\u007F", "reason": "DEL character is outside UCS allowed range" }, { - "value": "Ÿ", + "value": "\u009F", "reason": "Control character not part of ucschar" }, { - "value": "￾", + "value": "\uFFFE", "reason": "Noncharacter in Unicode" }, { - "value": "￿", + "value": "\uFFFF", "reason": "Noncharacter in Unicode" }, { @@ -25,7 +25,7 @@ "reason": "Null byte is not allowed" }, { - "value": "€", + "value": "\u0080", "reason": "C1 control character, disallowed" } ], @@ -535,7 +535,7 @@ "reason": "Invalid hex digit" }, { - "value": "1:2:3:4:5:6:7:💀", + "value": "1:2:3:4:5:6:7:\uD83D\uDC80", "reason": "Emoji is not valid in hex" }, { @@ -663,7 +663,7 @@ "reason": "Invalid character '#'" }, { - "value": "v1.💡", + "value": "v1.\uD83D\uDCA1", "reason": "Emoji is not allowed" }, { @@ -741,7 +741,7 @@ "reason": "Too many fields" }, { - "value": "[v1.💡]", + "value": "[v1.\uD83D\uDCA1]", "reason": "Emoji not allowed in IPvFuture" }, { @@ -1379,7 +1379,7 @@ "reason": "Newline character" }, { - "value": "💡", + "value": "\uD83D\uDCA1", "reason": "Emoji character" }, { @@ -1463,11 +1463,11 @@ "reason": "Space not allowed in iprivate" }, { - "value": "", + "value": "\u007F", "reason": "DEL character not in private range" }, { - "value": "ÿ", + "value": "\u00FF", "reason": "Extended Latin not private" }, { @@ -1475,63 +1475,63 @@ "reason": "NULL byte not private" }, { - "value": "豈", + "value": "\uF900", "reason": "CJK Compatibility Ideographs" }, { - "value": "퟿", + "value": "\uD7FF", "reason": "Upper limit of non-private BMP" }, { - "value": "￿", + "value": "\uFFFF", "reason": "Noncharacter" }, { - "value": "Ā", + "value": "\u0100", "reason": "Latin Extended-A" }, { - "value": "͸", + "value": "\u0378", "reason": "Unassigned code point" }, { - "value": "΀", + "value": "\u0380", "reason": "Unassigned code point" }, { - "value": "ჿFF", + "value": "\u10FF\u0046\u0046", "reason": "Outside defined private planes" }, { - "value": "", + "value": "\uFEFF", "reason": "Byte Order Mark" }, { - "value": "💡", + "value": "\uD83D\uDCA1", "reason": "Emoji not in private range" }, { - "value": "🧃", + "value": "\uD83E\uDDC3", "reason": "Emoji not in private range" }, { - "value": "💀", + "value": "\uD83D\uDC80", "reason": "Emoji not in private range" }, { - "value": "🔥", + "value": "\uD83D\uDD25", "reason": "Emoji not in private range" }, { - "value": "​", + "value": "\u200B", "reason": "Zero-width space not private" }, { - "value": "‮", + "value": "\u202E", "reason": "Bidi override character not private" }, { - "value": "⁠", + "value": "\u2060", "reason": "Word joiner character not private" } ], @@ -1639,60 +1639,6 @@ "reason": "Colon in host" } ], - "gen_delims": [ - { - "value": ":path", - "reason": "Colon used as prefix" - }, - { - "value": "/dir//file", - "reason": "Double slash" - }, - { - "value": "param#extra", - "reason": "Fragment not part of param" - }, - { - "value": "query?again", - "reason": "Double query markers" - }, - { - "value": "#frag1#frag2", - "reason": "Multiple fragments" - }, - { - "value": "[host", - "reason": "Unclosed bracket" - }, - { - "value": "]host", - "reason": "Bracket without match" - }, - { - "value": "user@:host", - "reason": "Colon in host" - }, - { - "value": "mailto:user@example.com#", - "reason": "Dangling fragment" - }, - { - "value": ":@", - "reason": "Misuse of delimiters" - }, - { - "value": "/?#@", - "reason": "Invalid sequence of gen-delims" - }, - { - "value": "[:]", - "reason": "Unescaped gen-delims in host" - }, - { - "value": "http://[host]", - "reason": "Improper IPv6 use" - } - ], "ifragment": [ { "value": "frag ment", @@ -1747,11 +1693,208 @@ "reason": "Caret in fragment" } ], - "isegment_nz_nc": [], - "ipath_noscheme": [], - "ipath": [], - "irelative_part": [], - "irelative_ref": [], + "isegment_nz_nc": [ + { + "value": "", + "reason": "Empty string: isegment-nz-nc requires at least one char (1+)" + }, + { + "value": ":", + "reason": "Colon forbidden in isegment-nz-nc (no-colon segment constraint)" + }, + { + "value": "a:b", + "reason": "Colon within the segment violates no-colon constraint" + }, + { + "value": " ", + "reason": "ASCII space (U+0020) not in iunreserved / pct-encoded / sub-delims / '@'" + }, + { + "value": "#", + "reason": "Fragment delimiter # is a gen-delim, not allowed in isegment-nz-nc" + }, + { + "value": "[", + "reason": "Left bracket is a gen-delim, not allowed in isegment-nz-nc" + }, + { + "value": "]", + "reason": "Right bracket is a gen-delim, not allowed in isegment-nz-nc" + }, + { + "value": "%GG", + "reason": "Invalid pct-encoded: G is not a hex digit" + }, + { + "value": "|", + "reason": "Pipe is not in the allowed character set for isegment-nz-nc" + }, + { + "value": "\\", + "reason": "Backslash is not in the allowed set for isegment-nz-nc" + }, + { + "value": "/", + "reason": "Slash separates segments; not part of a single isegment-nz-nc" + } + ], + "ipath_noscheme": [ + { + "value": ":segment", + "reason": "Colon starts first segment; colon forbidden in isegment-nz-nc" + }, + { + "value": "a:b", + "reason": "Colon in the sole first segment violates isegment-nz-nc" + }, + { + "value": "/segment", + "reason": "ipath-noscheme must not begin with '/'; that is ipath-abempty or ipath-absolute" + }, + { + "value": "//segment", + "reason": "Double slash is not a valid start for ipath-noscheme" + }, + { + "value": "seg ment", + "reason": "ASCII space (U+0020) not allowed in path segment (U+00A0 NO-BREAK SPACE as ucschar would be valid)" + }, + { + "value": "seg#ment", + "reason": "Hash introduces a fragment delimiter; not part of ipath" + }, + { + "value": "seg[ment", + "reason": "Left bracket is a gen-delim, forbidden in path segments" + }, + { + "value": "seg|ment", + "reason": "Pipe is not allowed in path segment" + }, + { + "value": "seg%GG/sub", + "reason": "Invalid pct-encoded sequence in first segment" + } + ], + "ipath": [ + { + "value": "seg ment", + "reason": "ASCII space (U+0020) is not allowed in any ipath alternative (U+00A0 NO-BREAK SPACE as ucschar would be valid)" + }, + { + "value": "seg|ment", + "reason": "Pipe is not allowed in any path segment" + }, + { + "value": "seg\\ment", + "reason": "Backslash is not allowed in path segments" + }, + { + "value": "seg[ment", + "reason": "Left bracket is a gen-delim, forbidden in path" + }, + { + "value": "seg]ment", + "reason": "Right bracket is a gen-delim, forbidden in path" + }, + { + "value": "seg{ment", + "reason": "Left curly brace not allowed in path" + }, + { + "value": "seg}ment", + "reason": "Right curly brace not allowed in path" + }, + { + "value": "seg^ment", + "reason": "Caret not allowed in path" + }, + { + "value": "seg\tment", + "reason": "Tab character not allowed in path" + }, + { + "value": "seg\nment", + "reason": "Newline not allowed in path" + }, + { + "value": "/seg%GG", + "reason": "Invalid pct-encoded sequence in path segment" + } + ], + "irelative_part": [ + { + "value": "//example.com:abc", + "reason": "Non-numeric port in authority" + }, + { + "value": "//exa mple.com", + "reason": "Space in reg-name within authority" + }, + { + "value": "/seg ment", + "reason": "Space in ipath-absolute segment" + }, + { + "value": ":segment", + "reason": "Colon starts ipath-noscheme first segment, forbidden by isegment-nz-nc" + }, + { + "value": "a:b", + "reason": "Colon in first segment of ipath-noscheme alternative: forbidden" + }, + { + "value": "seg|ment", + "reason": "Pipe not allowed in path segment" + }, + { + "value": "//host/%GG", + "reason": "Invalid pct-encoded in authority path" + }, + { + "value": "//[v1.]", + "reason": "Malformed IPvFuture in IP-literal (suffix missing after '.')" + } + ], + "irelative_ref": [ + { + "value": "//example.com/path#frag1#frag2", + "reason": "Multiple fragment delimiters; only one '#' allowed per IRI" + }, + { + "value": "//host path", + "reason": "Unencoded space in relative ref" + }, + { + "value": "//example.com/%", + "reason": "Incomplete pct-encoded sequence in path" + }, + { + "value": "//example.com:abc/path", + "reason": "Non-numeric port in relative ref authority" + }, + { + "value": "a:b", + "reason": "Colon in first segment of irelative-part: invalid as ipath-noscheme" + }, + { + "value": ":path", + "reason": "Starts with colon: invalid first char for ipath-noscheme (isegment-nz-nc)" + }, + { + "value": "seg\nment", + "reason": "Newline not allowed in irelative-ref" + }, + { + "value": "seg|ment", + "reason": "Pipe not allowed in irelative-ref path" + }, + { + "value": "seg%GG", + "reason": "Invalid pct-encoded sequence in irelative-ref" + } + ], "iri": [ { "value": "http://example.com/%ZZ", @@ -1810,20 +1953,6 @@ "reason": "Improper IPv6 format" } ], - "reserved": [ - { - "value": " ", - "reason": "Space is not a reserved character" - }, - { - "value": "%", - "reason": "Requires encoding" - }, - { - "value": "💡", - "reason": "Emoji is not a reserved character" - } - ], "scheme": [ { "value": "1http", @@ -1886,7 +2015,7 @@ "reason": "Contains tab" }, { - "value": "http🙂", + "value": "http\uD83D\uDE42", "reason": "Contains emoji" }, { @@ -2066,4 +2195,4 @@ "reason": "Only three octets" } ] -} \ No newline at end of file +} diff --git a/tests/_data/valid_syntax.json b/tests/_data/valid_syntax.json index 3d628d5..ffb6690 100644 --- a/tests/_data/valid_syntax.json +++ b/tests/_data/valid_syntax.json @@ -1,53 +1,1151 @@ { - "ucschar": [], - "iunreserved": [], - "sub_delims": [], - "iuserinfo": [], - "h16": [], - "dec_octet": [], - "ls32": [], - "ipv6address": [], - "unreserved": [], - "ipvfuture": [], - "ip_literal": [], - "ireg_name": [], - "ihost": [], - "port": [], - "iauthority": [], - "ipchar": [], - "isegment": [], - "ipath_abempty": [], - "isegment_nz": [], - "ipath_absolute": [], - "ipath_rootless": [], - "ipath_empty": [], - "ihier_part": [], - "iprivate": [], - "iquery": [], + "ucschar": [ + { + "value": "\u00A0", + "reason": "U+00A0 NO-BREAK SPACE: boundary start of ucschar range %xA0-D7FF" + }, + { + "value": "\u4E2D", + "reason": "U+4E2D CJK: within ucschar range %xA0-D7FF" + }, + { + "value": "\uD7FF", + "reason": "U+D7FF: boundary end of ucschar range %xA0-D7FF" + }, + { + "value": "\uF900", + "reason": "U+F900: boundary start of ucschar range %xF900-FDCF" + }, + { + "value": "\uFDCF", + "reason": "U+FDCF: boundary end of ucschar range %xF900-FDCF" + }, + { + "value": "\uFDF0", + "reason": "U+FDF0: boundary start of ucschar range %xFDF0-FFEF" + }, + { + "value": "\uFFEF", + "reason": "U+FFEF: boundary end of ucschar range %xFDF0-FFEF" + }, + { + "value": "\uD800\uDC00", + "reason": "U+10000: boundary start of supplementary ucschar range %x10000-1FFFD" + }, + { + "value": "\uD83F\uDFFD", + "reason": "U+1FFFD: boundary end of supplementary ucschar range %x10000-1FFFD" + }, + { + "value": "\uD840\uDC00", + "reason": "U+20000: boundary start of supplementary ucschar range %x20000-2FFFD" + }, + { + "value": "\uD87F\uDFFD", + "reason": "U+2FFFD: boundary end of supplementary ucschar range %x20000-2FFFD" + }, + { + "value": "\uDB44\uDC00", + "reason": "U+E1000: boundary start of supplementary ucschar range %xE1000-EFFFD" + }, + { + "value": "\uDB7F\uDFFD", + "reason": "U+EFFFD: boundary end of supplementary ucschar range %xE1000-EFFFD" + } + ], + "iunreserved": [ + { + "value": "a", + "reason": "Lowercase ALPHA is unreserved" + }, + { + "value": "Z", + "reason": "Uppercase ALPHA is unreserved" + }, + { + "value": "0", + "reason": "DIGIT 0 is unreserved" + }, + { + "value": "9", + "reason": "DIGIT 9 is unreserved" + }, + { + "value": "-", + "reason": "Hyphen is unreserved" + }, + { + "value": ".", + "reason": "Period is unreserved" + }, + { + "value": "_", + "reason": "Underscore is unreserved" + }, + { + "value": "~", + "reason": "Tilde is unreserved" + }, + { + "value": "\u00A0", + "reason": "ucschar U+00A0 via iunreserved (ucschar range %xA0-D7FF)" + }, + { + "value": "\u4E2D", + "reason": "ucschar CJK U+4E2D via iunreserved" + } + ], + "sub_delims": [ + { + "value": "!", + "reason": "Exclamation mark is a sub-delim" + }, + { + "value": "$", + "reason": "Dollar sign is a sub-delim" + }, + { + "value": "&", + "reason": "Ampersand is a sub-delim" + }, + { + "value": "'", + "reason": "Single quote is a sub-delim" + }, + { + "value": "(", + "reason": "Left parenthesis is a sub-delim" + }, + { + "value": ")", + "reason": "Right parenthesis is a sub-delim" + }, + { + "value": "*", + "reason": "Asterisk is a sub-delim" + }, + { + "value": "+", + "reason": "Plus sign is a sub-delim" + }, + { + "value": ",", + "reason": "Comma is a sub-delim" + }, + { + "value": ";", + "reason": "Semicolon is a sub-delim" + }, + { + "value": "=", + "reason": "Equals sign is a sub-delim" + } + ], + "iuserinfo": [ + { + "value": "", + "reason": "Empty iuserinfo is valid (zero or more chars per RFC)" + }, + { + "value": "user", + "reason": "Simple username" + }, + { + "value": "user:password", + "reason": "Username:password (colon is allowed in iuserinfo)" + }, + { + "value": "user%40name", + "reason": "Percent-encoded at-sign in iuserinfo" + }, + { + "value": "user:pass:extra", + "reason": "Multiple colons allowed in iuserinfo" + }, + { + "value": "!$&'()*+,;=", + "reason": "All sub-delims valid in iuserinfo" + }, + { + "value": "\u7528\u6237", + "reason": "Chinese characters (ucschar) in iuserinfo" + }, + { + "value": "user%20name", + "reason": "Percent-encoded space in iuserinfo" + } + ], + "h16": [ + { + "value": "0", + "reason": "Single hex digit (minimum: 1HEXDIG)" + }, + { + "value": "f", + "reason": "Single lowercase hex digit" + }, + { + "value": "F", + "reason": "Single uppercase hex digit" + }, + { + "value": "FF", + "reason": "Two hex digits" + }, + { + "value": "A1B", + "reason": "Three hex digits" + }, + { + "value": "FFFF", + "reason": "Four hex digits (maximum: 4HEXDIG)" + }, + { + "value": "0000", + "reason": "Four zero digits" + }, + { + "value": "abcd", + "reason": "Lowercase 4-digit h16" + }, + { + "value": "1a2b", + "reason": "Mixed-case 4-digit h16" + } + ], + "dec_octet": [ + { + "value": "0", + "reason": "Single digit 0 (DIGIT form)" + }, + { + "value": "9", + "reason": "Single digit 9 (DIGIT form, max boundary)" + }, + { + "value": "10", + "reason": "Two-digit minimum (%x31-39 DIGIT form, 10)" + }, + { + "value": "99", + "reason": "Two-digit maximum (%x31-39 DIGIT form boundary)" + }, + { + "value": "100", + "reason": "Minimum for '1' 2DIGIT form" + }, + { + "value": "199", + "reason": "Maximum for '1' 2DIGIT form (boundary)" + }, + { + "value": "200", + "reason": "Minimum for '2' %x30-34 DIGIT form" + }, + { + "value": "249", + "reason": "Maximum for '2' %x30-34 DIGIT form (boundary)" + }, + { + "value": "250", + "reason": "Minimum for '25' %x30-35 form" + }, + { + "value": "255", + "reason": "Maximum dec-octet: '25' %x30-35 boundary" + } + ], + "ls32": [ + { + "value": "FFFF:FFFF", + "reason": "h16:h16 form with maximum values" + }, + { + "value": "0:0", + "reason": "Minimal h16:h16" + }, + { + "value": "a:b", + "reason": "Single hex digit each side (h16:h16)" + }, + { + "value": "abcd:ef01", + "reason": "Four-digit h16:h16" + }, + { + "value": "192.168.1.1", + "reason": "IPv4address form of ls32" + }, + { + "value": "0.0.0.0", + "reason": "All-zero IPv4 as ls32" + }, + { + "value": "255.255.255.255", + "reason": "Maximum IPv4 as ls32" + } + ], + "ipv6address": [ + { + "value": "2001:db8:85a3:0:0:8a2e:370:7334", + "reason": "Form 1: 6(h16:) ls32 \u2014 fully explicit 8-group" + }, + { + "value": "::1:2:3:4:5:6:7", + "reason": "Form 2: :: 5(h16:) ls32 \u2014 leading :: with five h16 groups then ls32" + }, + { + "value": "::1:2:3:4:192.168.1.1", + "reason": "Form 3: :: 4(h16:) ls32 with IPv4 ls32, no h16 before ::" + }, + { + "value": "2001::1:2:3:192.168.1.1", + "reason": "Form 3 variant: one h16 before ::, 4(h16:) ls32" + }, + { + "value": "2001:db8::1:2:192.168.1.1", + "reason": "Form 4: *1(h16:) h16 :: 3(h16:) ls32" + }, + { + "value": "::ffff:192.168.1.1", + "reason": "Form 6: :: h16: ls32 \u2014 IPv4-mapped, no h16 before ::" + }, + { + "value": "2001:db8:85a3::192.168.1.1", + "reason": "Form 7: *4(h16:) h16 :: ls32 with IPv4" + }, + { + "value": "::1", + "reason": "Form 8: :: h16 \u2014 IPv6 loopback" + }, + { + "value": "2001:db8::1", + "reason": "Form 8 variant: *1(h16:) h16 :: h16" + }, + { + "value": "::", + "reason": "Form 9: :: alone \u2014 all-zeros compressed" + }, + { + "value": "2001::", + "reason": "Form 9 variant: single h16 followed by ::" + }, + { + "value": "fe80::1", + "reason": "Form 8: link-local prefix with :: h16" + }, + { + "value": "64:ff9b::192.168.1.1", + "reason": "Form 7: IPv4-translated via :: and IPv4 ls32" + } + ], + "unreserved": [ + { + "value": "a", + "reason": "Lowercase ALPHA is unreserved" + }, + { + "value": "Z", + "reason": "Uppercase ALPHA is unreserved" + }, + { + "value": "0", + "reason": "DIGIT 0 is unreserved" + }, + { + "value": "9", + "reason": "DIGIT 9 is unreserved" + }, + { + "value": "-", + "reason": "Hyphen is unreserved" + }, + { + "value": ".", + "reason": "Period is unreserved" + }, + { + "value": "_", + "reason": "Underscore is unreserved" + }, + { + "value": "~", + "reason": "Tilde is unreserved" + } + ], + "ipvfuture": [ + { + "value": "v1.a", + "reason": "Minimal IPvFuture: version v1 with single unreserved char" + }, + { + "value": "v1.test", + "reason": "IPvFuture version 1 with unreserved suffix" + }, + { + "value": "vFF.abcd", + "reason": "IPvFuture with multi-hex version number" + }, + { + "value": "v1.~-._", + "reason": "IPvFuture suffix with unreserved chars ~-._" + }, + { + "value": "v1.!$&'()*+,;=", + "reason": "IPvFuture suffix with all sub-delims" + }, + { + "value": "v1.a:b", + "reason": "IPvFuture suffix with colon (colon allowed per RFC)" + }, + { + "value": "v123.host", + "reason": "IPvFuture with multi-digit hex version" + } + ], + "ip_literal": [ + { + "value": "[::1]", + "reason": "IPv6 loopback in brackets" + }, + { + "value": "[::]", + "reason": "All-zero compressed IPv6 in brackets" + }, + { + "value": "[2001:db8::1]", + "reason": "Typical IPv6 documentation address in brackets" + }, + { + "value": "[2001:db8:85a3:0:0:8a2e:370:7334]", + "reason": "Full explicit IPv6 in brackets" + }, + { + "value": "[::ffff:192.168.1.1]", + "reason": "IPv4-mapped IPv6 address in brackets" + }, + { + "value": "[v1.test]", + "reason": "Minimal IPvFuture in brackets" + }, + { + "value": "[vFF.a:b]", + "reason": "IPvFuture with hex version and colon in suffix" + }, + { + "value": "[v1.!$&'()*+,;=]", + "reason": "IPvFuture with sub-delims in suffix" + } + ], + "ireg_name": [ + { + "value": "", + "reason": "Empty reg-name is valid (zero or more chars per RFC)" + }, + { + "value": "example.com", + "reason": "Typical hostname as reg-name" + }, + { + "value": "xn--nxasmq6b.com", + "reason": "Punycode-encoded hostname label" + }, + { + "value": "%20example", + "reason": "Percent-encoded character in reg-name" + }, + { + "value": "\u043F\u0440\u0438\u043C\u0435\u0440", + "reason": "Cyrillic reg-name (ucschar via iunreserved)" + }, + { + "value": "example!test", + "reason": "Sub-delim ! allowed in reg-name" + }, + { + "value": "a$b&c", + "reason": "Multiple sub-delims in reg-name" + }, + { + "value": "\u4E2D\u6587", + "reason": "CJK characters (ucschar) in reg-name" + } + ], + "ihost": [ + { + "value": "example.com", + "reason": "Typical hostname (ireg-name)" + }, + { + "value": "", + "reason": "Empty host (ireg-name may be zero chars)" + }, + { + "value": "192.168.1.1", + "reason": "IPv4address as ihost" + }, + { + "value": "[::1]", + "reason": "IPv6 loopback as IP-literal" + }, + { + "value": "[2001:db8::1]", + "reason": "IPv6 documentation address as IP-literal" + }, + { + "value": "[v1.test]", + "reason": "IPvFuture as IP-literal" + }, + { + "value": "\u4E2D\u6587.\u516C\u53F8", + "reason": "IDN-style host with CJK ucschar in reg-name" + } + ], + "port": [ + { + "value": "", + "reason": "Empty port is valid (port = *DIGIT, zero or more)" + }, + { + "value": "0", + "reason": "Port zero" + }, + { + "value": "80", + "reason": "Standard HTTP port" + }, + { + "value": "443", + "reason": "Standard HTTPS port" + }, + { + "value": "8080", + "reason": "Common alternate HTTP port" + }, + { + "value": "65535", + "reason": "Maximum 16-bit port (RFC does not restrict value range)" + }, + { + "value": "00", + "reason": "Leading zeros allowed (port = *DIGIT, no leading-zero restriction)" + } + ], + "iauthority": [ + { + "value": "example.com", + "reason": "Host only (no userinfo, no port)" + }, + { + "value": "", + "reason": "Empty authority (empty ireg-name, no port)" + }, + { + "value": "user@example.com", + "reason": "Userinfo and host" + }, + { + "value": "example.com:80", + "reason": "Host with numeric port" + }, + { + "value": "user:pass@example.com:443", + "reason": "Full authority: userinfo, host, port" + }, + { + "value": "[::1]", + "reason": "IPv6 literal as host" + }, + { + "value": "[::1]:8080", + "reason": "IPv6 literal host with port" + }, + { + "value": "\u7528\u6237@\u4E2D\u6587.com:8080", + "reason": "IRI authority with ucschar in userinfo and host" + } + ], + "ipchar": [ + { + "value": "a", + "reason": "ALPHA via iunreserved" + }, + { + "value": "Z", + "reason": "Uppercase ALPHA via iunreserved" + }, + { + "value": "0", + "reason": "DIGIT via iunreserved" + }, + { + "value": "-", + "reason": "Hyphen via iunreserved" + }, + { + "value": ".", + "reason": "Period via iunreserved" + }, + { + "value": "_", + "reason": "Underscore via iunreserved" + }, + { + "value": "~", + "reason": "Tilde via iunreserved" + }, + { + "value": "\u00A0", + "reason": "ucschar U+00A0 via iunreserved" + }, + { + "value": "%20", + "reason": "Percent-encoded space via pct-encoded" + }, + { + "value": "%2F", + "reason": "Percent-encoded slash via pct-encoded" + }, + { + "value": "!", + "reason": "Sub-delim ! via sub-delims" + }, + { + "value": "$", + "reason": "Sub-delim $ via sub-delims" + }, + { + "value": "&", + "reason": "Sub-delim & via sub-delims" + }, + { + "value": "'", + "reason": "Sub-delim ' via sub-delims" + }, + { + "value": "(", + "reason": "Sub-delim ( via sub-delims" + }, + { + "value": ")", + "reason": "Sub-delim ) via sub-delims" + }, + { + "value": "*", + "reason": "Sub-delim * via sub-delims" + }, + { + "value": "+", + "reason": "Sub-delim + via sub-delims" + }, + { + "value": ",", + "reason": "Sub-delim , via sub-delims" + }, + { + "value": ";", + "reason": "Sub-delim ; via sub-delims" + }, + { + "value": "=", + "reason": "Sub-delim = via sub-delims" + }, + { + "value": ":", + "reason": "Colon is directly allowed in ipchar" + }, + { + "value": "@", + "reason": "At-sign is directly allowed in ipchar" + } + ], + "isegment": [ + { + "value": "", + "reason": "Empty segment is valid (isegment = *ipchar, zero or more)" + }, + { + "value": "a", + "reason": "Single-char segment" + }, + { + "value": "segment", + "reason": "Multi-char segment" + }, + { + "value": "seg:ment", + "reason": "Segment with colon (ipchar allows colon)" + }, + { + "value": "seg@ment", + "reason": "Segment with at-sign (ipchar allows @)" + }, + { + "value": "%20", + "reason": "Percent-encoded segment" + }, + { + "value": "\u00FCber", + "reason": "Segment with ucschar (\u00FC)" + }, + { + "value": "seg!$&'()*+,;=", + "reason": "Segment with all sub-delims" + } + ], + "ipath_abempty": [ + { + "value": "", + "reason": "Empty ipath-abempty (zero repetitions of / isegment)" + }, + { + "value": "/", + "reason": "Single slash with empty segment" + }, + { + "value": "/segment", + "reason": "Single non-empty segment" + }, + { + "value": "/a/b/c", + "reason": "Three-level path" + }, + { + "value": "//double", + "reason": "Double slash: first empty segment, then 'double'" + }, + { + "value": "/\u00FCber/path", + "reason": "Path with ucschar in segment" + }, + { + "value": "/seg:ment", + "reason": "Colon in segment (ipchar allows colon in ipath-abempty)" + }, + { + "value": "/seg%20ment", + "reason": "Percent-encoded space in segment" + } + ], + "isegment_nz": [ + { + "value": "a", + "reason": "Minimal single-char non-zero segment" + }, + { + "value": "segment", + "reason": "Multi-char segment" + }, + { + "value": "seg:ment", + "reason": "Segment with colon (ipchar allows colon)" + }, + { + "value": "%20", + "reason": "Percent-encoded non-empty segment" + }, + { + "value": "\u00FCber", + "reason": "Non-zero segment with ucschar" + }, + { + "value": "seg!$&'()*+,;=:@", + "reason": "Segment with sub-delims, colon, at-sign" + } + ], + "ipath_absolute": [ + { + "value": "/", + "reason": "Just slash: ipath-absolute with no optional isegment-nz" + }, + { + "value": "/segment", + "reason": "Single segment after slash" + }, + { + "value": "/a/b/c", + "reason": "Multi-level absolute path" + }, + { + "value": "/\u00FCber/path", + "reason": "Absolute path with ucschar" + }, + { + "value": "/seg:ment/sub", + "reason": "Colon in segment (ipath-absolute allows colon)" + }, + { + "value": "/%20/path", + "reason": "Percent-encoded first segment" + }, + { + "value": "/seg!$&'()*+,;=", + "reason": "Sub-delims in absolute path segment" + } + ], + "ipath_rootless": [ + { + "value": "a", + "reason": "Minimal single-char rootless path" + }, + { + "value": "segment", + "reason": "Single segment" + }, + { + "value": "a/b/c", + "reason": "Multi-segment rootless path" + }, + { + "value": "\u00FCber/path", + "reason": "Rootless path with ucschar" + }, + { + "value": "seg:ment", + "reason": "Colon allowed in isegment-nz of ipath-rootless" + }, + { + "value": "%20/path", + "reason": "Percent-encoded first segment" + }, + { + "value": "scheme:like/path", + "reason": "Colon in first segment (valid rootless path)" + } + ], + "ipath_empty": [ + { + "value": "", + "reason": "Empty string is the only valid ipath-empty (0 ipchars per RFC)" + } + ], + "ihier_part": [ + { + "value": "//example.com/path", + "reason": "Form 1: // iauthority ipath-abempty" + }, + { + "value": "//example.com", + "reason": "Form 1: // iauthority with empty ipath-abempty" + }, + { + "value": "//user@example.com:80/path", + "reason": "Form 1: authority with userinfo and port" + }, + { + "value": "/absolute", + "reason": "Form 2: ipath-absolute" + }, + { + "value": "/a/b/c", + "reason": "Form 2: multi-level ipath-absolute" + }, + { + "value": "rootless", + "reason": "Form 3: ipath-rootless" + }, + { + "value": "seg:ment", + "reason": "Form 3: ipath-rootless with colon in segment" + }, + { + "value": "", + "reason": "Form 4: ipath-empty" + } + ], + "iprivate": [ + { + "value": "\uE000", + "reason": "U+E000: boundary start of iprivate range %xE000-F8FF" + }, + { + "value": "\uF8FF", + "reason": "U+F8FF: boundary end of iprivate range %xE000-F8FF" + }, + { + "value": "\uDB80\uDC00", + "reason": "U+F0000: boundary start of iprivate range %xF0000-FFFFD" + }, + { + "value": "\uDBBF\uDFFD", + "reason": "U+FFFFD: boundary end of iprivate range %xF0000-FFFFD" + }, + { + "value": "\uDBC0\uDC00", + "reason": "U+100000: boundary start of iprivate range %x100000-10FFFD" + }, + { + "value": "\uDBFF\uDFFD", + "reason": "U+10FFFD: boundary end of iprivate range %x100000-10FFFD" + }, + { + "value": "\uE001", + "reason": "U+E001: mid-range within %xE000-F8FF iprivate" + }, + { + "value": "\uF000", + "reason": "U+F000: mid-range within %xE000-F8FF iprivate" + } + ], + "iquery": [ + { + "value": "", + "reason": "Empty query is valid (iquery = *(ipchar / iprivate / '/' / '?'))" + }, + { + "value": "q=1", + "reason": "Simple key=value query" + }, + { + "value": "key=value&key2=value2", + "reason": "Multiple params (& is sub-delim in ipchar)" + }, + { + "value": "/path", + "reason": "Slash allowed in iquery" + }, + { + "value": "?nested", + "reason": "Question mark allowed in iquery" + }, + { + "value": "\u00FCber", + "reason": "ucschar (U+00FC) in query via iunreserved" + }, + { + "value": "\uE000", + "reason": "iprivate char U+E000 is allowed in iquery" + }, + { + "value": "q=a!b$c&d=e'f(g)h*i+j,k;l=m", + "reason": "All sub-delims in query" + }, + { + "value": "seg:ment@host", + "reason": "Colon and at-sign in query (via ipchar)" + }, + { + "value": "q=%20value", + "reason": "Percent-encoded value in query" + } + ], "absolute_iri": [ { - "value": "ftp://пример.испытание.example.com/файл.txt", + "value": "ftp://\u043F\u0440\u0438\u043C\u0435\u0440.\u0438\u0441\u043F\u044B\u0442\u0430\u043D\u0438\u0435.example.com/\u0444\u0430\u0439\u043B.txt", "expect_lark": true, "reason": "" }, { - "value": "https://www.example.com/über", + "value": "https://www.example.com/\u00FCber", "expect_lark": true, "reason": "" }, { - "value": "https://exämple.example.org/pageセクション1", + "value": "https://ex\u00E4mple.example.org/page\u30BB\u30AF\u30B7\u30E7\u30F31", "expect_lark": true, "reason": "" } ], - "gen_delims": [], - "ifragment": [], - "isegment_nz_nc": [], - "ipath_noscheme": [], - "ipath": [], - "irelative_part": [], - "irelative_ref": [], + "ifragment": [ + { + "value": "", + "reason": "Empty fragment is valid (ifragment = *(ipchar / '/' / '?'))" + }, + { + "value": "section1", + "reason": "Simple alphanumeric fragment" + }, + { + "value": "\u00FCber", + "reason": "Fragment with ucschar U+00FC" + }, + { + "value": "/path", + "reason": "Slash allowed in ifragment" + }, + { + "value": "?query", + "reason": "Question mark allowed in ifragment" + }, + { + "value": "seg:ment", + "reason": "Colon allowed in ifragment (via ipchar)" + }, + { + "value": "%20", + "reason": "Percent-encoded space in fragment" + }, + { + "value": "a!b$c&d=e'f(g)h*i+j,k;l=m", + "reason": "All sub-delims in fragment (via ipchar)" + }, + { + "value": "seg@host", + "reason": "At-sign in fragment (via ipchar)" + } + ], + "isegment_nz_nc": [ + { + "value": "a", + "reason": "Minimal single-char no-colon non-zero segment" + }, + { + "value": "segment", + "reason": "Multi-char no-colon segment" + }, + { + "value": "seg@ment", + "reason": "At-sign allowed in isegment-nz-nc" + }, + { + "value": "%20", + "reason": "Percent-encoded no-colon segment" + }, + { + "value": "\u00FCber", + "reason": "No-colon segment with ucschar" + }, + { + "value": "seg!$&'()*+,;=", + "reason": "Sub-delims in no-colon segment (no colon)" + }, + { + "value": "a@b@c", + "reason": "Multiple at-signs allowed (isegment-nz-nc allows @)" + } + ], + "ipath_noscheme": [ + { + "value": "a", + "reason": "Minimal: single isegment-nz-nc" + }, + { + "value": "segment", + "reason": "Single no-colon first segment" + }, + { + "value": "a/b/c", + "reason": "Multi-segment noscheme path" + }, + { + "value": "\u00FCber/path", + "reason": "Noscheme path with ucschar in first segment" + }, + { + "value": "@user/info", + "reason": "At-sign in first segment (isegment-nz-nc allows @)" + }, + { + "value": "seg!ment/sub", + "reason": "Sub-delim in first segment" + }, + { + "value": "a/b:c", + "reason": "Colon allowed in later segments (isegment allows colon)" + }, + { + "value": "%20/path", + "reason": "Percent-encoded first segment" + } + ], + "ipath": [ + { + "value": "", + "reason": "ipath-empty form" + }, + { + "value": "/", + "reason": "ipath-abempty with single empty segment" + }, + { + "value": "/segment", + "reason": "ipath-abempty or ipath-absolute" + }, + { + "value": "/a/b/c", + "reason": "Multi-level ipath-absolute" + }, + { + "value": "a/b", + "reason": "ipath-noscheme or ipath-rootless" + }, + { + "value": "seg:ment", + "reason": "ipath-rootless (colon OK in isegment-nz)" + }, + { + "value": "//empty/authority/form", + "reason": "ipath-abempty with double empty first segment" + }, + { + "value": "\u00FCber/path", + "reason": "ipath-noscheme with ucschar in first segment" + } + ], + "irelative_part": [ + { + "value": "//example.com/path", + "reason": "Form 1: // iauthority ipath-abempty" + }, + { + "value": "//example.com", + "reason": "Form 1: // iauthority with empty ipath-abempty" + }, + { + "value": "//user@example.com:80/path", + "reason": "Form 1: full authority in irelative-part" + }, + { + "value": "/absolute", + "reason": "Form 2: ipath-absolute" + }, + { + "value": "segment", + "reason": "Form 3: ipath-noscheme (no colon in first segment)" + }, + { + "value": "a/b", + "reason": "Form 3: ipath-noscheme multi-segment" + }, + { + "value": "", + "reason": "Form 4: ipath-empty" + } + ], + "irelative_ref": [ + { + "value": "", + "reason": "Empty irelative-ref (all parts can be empty)" + }, + { + "value": "//example.com/path?q=1#frag", + "reason": "Full relative ref with authority, query, fragment" + }, + { + "value": "/path?q=1", + "reason": "Absolute path with query" + }, + { + "value": "\u00FCber#frag", + "reason": "No-colon first segment with fragment" + }, + { + "value": "?q=1", + "reason": "Query only (empty irelative-part)" + }, + { + "value": "#frag", + "reason": "Fragment only (empty irelative-part and query)" + }, + { + "value": "//[::1]/path", + "reason": "IPv6 literal host in relative ref" + }, + { + "value": "a/b/c?key=val#section", + "reason": "Noscheme path with query and fragment" + } + ], "iri": [ { "value": "http:///example.com", @@ -63,49 +1161,69 @@ "expect_lark": true }, { - "value": "mailto:用户@例子.公司@example.com", + "value": "mailto:\u7528\u6237@\u4F8B\u5B50.\u516C\u53F8@example.com", "expect_lark": true, "reason": "" }, { - "value": "ftp://пример.испытание.example.com/файл.txt", + "value": "ftp://\u043F\u0440\u0438\u043C\u0435\u0440.\u0438\u0441\u043F\u044B\u0442\u0430\u043D\u0438\u0435.example.com/\u0444\u0430\u0439\u043B.txt", "expect_lark": true, "reason": "" }, { - "value": "https://www.example.com/über", + "value": "https://www.example.com/\u00FCber", "expect_lark": true, "reason": "" }, { - "value": "https://exämple.example.org/page#セクション1", + "value": "https://ex\u00E4mple.example.org/page#\u30BB\u30AF\u30B7\u30E7\u30F31", "expect_lark": true, "reason": "" + }, + { + "value": "http://example.com?q=1&lang=\u00FCber#\u7B2C\u4E00\u7AE0", + "expect_lark": true, + "reason": "IRI with ucschar in query and fragment" + }, + { + "value": "http://[2001:db8::1]/path", + "expect_lark": true, + "reason": "IRI with IPv6 literal host" + }, + { + "value": "http://[v1.test]/path", + "expect_lark": true, + "reason": "IRI with IPvFuture host" + }, + { + "value": "s://x", + "expect_lark": true, + "reason": "Minimal valid IRI: single-char scheme, host as single-char ireg-name" } ], "iri_reference": [ { - "value": "/documents/नियम.pdf", + "value": "/documents/\u0928\u093F\u092F\u092E.pdf", "expect_lark": true, "reason": "" }, { - "value": "../../фото/лошадь.jpg", + "value": "../../\u0444\u043E\u0442\u043E/\u043B\u043E\u0448\u0430\u0434\u044C.jpg", "expect_lark": true, "reason": "" }, { - "value": "index.html#संपर्क", + "value": "index.html#\u0938\u0902\u092A\u0930\u094D\u0915", "expect_lark": true, "reason": "" }, { - "value": "doc/guide.html#überblick", + "value": "doc/guide.html#\u00FCberblick", "expect_lark": true, "reason": "" }, { - "value": "пример/тест#часть2", + "value": "\u043F\u0440\u0438\u043C\u0435\u0440/\u0442\u0435\u0441\u0442#\u0447\u0430\u0441\u0442\u044C2", "expect_lark": true, "reason": "" }, @@ -158,10 +1276,145 @@ "value": "//[2041:0:140F::875B:131B]/foo", "expect_lark": true, "reason": "`IPv6address` case: [ *4( h16 \":\" ) h16 ] \"::\" ls32" + }, + { + "value": "http://example.com", + "expect_lark": true, + "reason": "Simple IRI as iri-reference (IRI variant)" + }, + { + "value": "//example.com/path", + "expect_lark": true, + "reason": "Protocol-relative reference (irelative-ref with authority)" + }, + { + "value": "?q=1#frag", + "expect_lark": true, + "reason": "Query and fragment only (empty irelative-part)" + }, + { + "value": "#section", + "expect_lark": true, + "reason": "Fragment-only IRI reference" + }, + { + "value": "", + "expect_lark": true, + "reason": "Empty string is a valid IRI reference (empty irelative-ref)" + } + ], + "scheme": [ + { + "value": "http", + "reason": "Standard HTTP scheme (ALPHA *( ALPHA / DIGIT / '+' / '-' / '.' ))" + }, + { + "value": "ftp", + "reason": "FTP scheme" + }, + { + "value": "a", + "reason": "Minimal scheme: single ALPHA" + }, + { + "value": "a123", + "reason": "Scheme with digits after initial ALPHA" + }, + { + "value": "a+b", + "reason": "Scheme with plus (allowed after first char)" + }, + { + "value": "a-b", + "reason": "Scheme with hyphen (allowed after first char)" + }, + { + "value": "a.b", + "reason": "Scheme with period (allowed after first char)" + }, + { + "value": "HTTPS", + "reason": "Uppercase scheme (ALPHA is case-insensitive per RFC)" + }, + { + "value": "a+b-c.2", + "reason": "Scheme with all allowed post-initial chars" + }, + { + "value": "coap+tcp", + "reason": "Real scheme with plus (CoAP over TCP)" + } + ], + "pct_encoded": [ + { + "value": "%20", + "reason": "Percent-encoded space (0x20)" + }, + { + "value": "%2F", + "reason": "Percent-encoded slash (0x2F)" + }, + { + "value": "%FF", + "reason": "Percent-encoded byte 0xFF" + }, + { + "value": "%00", + "reason": "Percent-encoded null byte (0x00)" + }, + { + "value": "%9a", + "reason": "Lowercase hex digits in pct-encoded" + }, + { + "value": "%Ab", + "reason": "Mixed-case hex digits in pct-encoded" + }, + { + "value": "%0A", + "reason": "Percent-encoded newline (0x0A)" + }, + { + "value": "%7E", + "reason": "Percent-encoded tilde (0x7E)" } ], - "reserved": [], - "scheme": [], - "pct_encoded": [], - "ipv4address": [] + "ipv4address": [ + { + "value": "0.0.0.0", + "reason": "All-zero IPv4 address (minimum boundary)" + }, + { + "value": "255.255.255.255", + "reason": "All-max IPv4 address (maximum boundary)" + }, + { + "value": "192.168.1.1", + "reason": "Typical private-range IPv4" + }, + { + "value": "10.0.0.1", + "reason": "10.x private range" + }, + { + "value": "127.0.0.1", + "reason": "Loopback address" + }, + { + "value": "1.2.3.4", + "reason": "Simple ascending IPv4" + }, + { + "value": "100.200.249.0", + "reason": "Tests 2xx and 24x dec-octets" + }, + { + "value": "250.251.252.253", + "reason": "Tests 25x dec-octet range" + }, + { + "value": "9.99.199.255", + "reason": "Covers all five dec-octet grammar alternatives" + } + ] } diff --git a/tests/test_syntax.py b/tests/test_syntax.py index c096cc1..a3c30f2 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -2,14 +2,19 @@ # Copyright (c) 2026 Jan Kowalleck - modifications and maintenance # SPDX-License-Identifier: MIT -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable import pytest -from typing import Any, Callable import rfc3987_syntax2 as sut -from . import valid_syntax_data, invalid_syntax_data, SyntaxCase, T_syntax_file +from . import ( + TESTS_DATA_FILES, + valid_syntax_data, + invalid_syntax_data, + SyntaxCase, + T_syntax_file, +) def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]) -> Any: @@ -20,7 +25,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 = sut.is_valid_syntax(term=term, value=valid_example["value"]) + actual = getattr(sut, f'is_valid_syntax_{term}')(valid_example["value"]) print("") print( f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" @@ -32,7 +37,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 = sut.is_valid_syntax(term=term, value=invalid_example["value"]) + actual = getattr(sut, f'is_valid_syntax_{term}')(invalid_example["value"]) print("") print( f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}"