From 3d882e0a447b54dd49557625ac2888b49c70426d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 23 Nov 2025 16:20:10 +0000 Subject: [PATCH 01/98] Add redhat.vscode-yaml to recommended extensions --- .vscode/extensions.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 06ece4f..fbed416 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "charliermarsh.ruff", "github.vscode-github-actions", "ms-python.python", + "redhat.vscode-yaml", "rust-lang.rust-analyzer", "tamasfe.even-better-toml" ] From 2205e592447751dbea9cbd24da6b77325d21660c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Tue, 25 Nov 2025 23:36:05 +0000 Subject: [PATCH 02/98] Update Cargo.toml and pyproject.toml --- .github/workflows/CI.yml | 4 +- Cargo.lock | 51 +---------- Cargo.toml | 8 +- pyproject.toml | 5 +- tests/urlpatterntestdata.json | 166 +++++++++++++++++++++++++++++++++- 5 files changed, 175 insertions(+), 59 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 75afc32..1d062a5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,4 @@ -# This file is autogenerated by maturin v1.8.3 +# This file is autogenerated by maturin v1.10.2 # To update, run # # maturin generate-ci --pytest github @@ -231,7 +231,7 @@ jobs: runs-on: ubuntu-latest if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} needs: [linux, musllinux, windows, macos, sdist] - environment: release + environment: pypi permissions: # Use to sign the release artifacts id-token: write diff --git a/Cargo.lock b/Cargo.lock index 0d90500..fa64983 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,9 +376,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -412,47 +412,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicode-ident" version = "1.0.22" @@ -479,13 +438,13 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +checksum = "957a88ad1abd5d13336275adb17d4f9b6a2404f3baed2e075e0b026dc0b2b58d" dependencies = [ + "icu_properties", "regex", "serde", - "unic-ucd-ident", "url", ] diff --git a/Cargo.toml b/Cargo.toml index b0083be..648cb65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "python-urlpattern" version = "0.1.4" authors = ["방성범 (Bang Seongbeom) "] -edition = "2021" +edition = "2024" description = "An implementation of the URL Pattern Standard for Python written in Rust" repository = "https://github.com/urlpattern/python-urlpattern" license = "MIT" @@ -15,6 +15,6 @@ crate-type = ["cdylib"] [dependencies] pyo3 = "0.27.1" -regex = "1.11.0" -url = "2.5.0" -urlpattern = "0.3.0" +regex = "1.12.2" +url = "2.5.7" +urlpattern = "0.4.1" diff --git a/pyproject.toml b/pyproject.toml index 031401a..b423a52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=1.7,<2.0"] +requires = ["maturin>=1.10,<2.0"] build-backend = "maturin" [project] @@ -21,10 +21,11 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Rust", "Topic :: Internet :: WWW/HTTP", "Typing :: Typed", ] diff --git a/tests/urlpatterntestdata.json b/tests/urlpatterntestdata.json index aeb0cd6..6b3fe0d 100644 --- a/tests/urlpatterntestdata.json +++ b/tests/urlpatterntestdata.json @@ -1202,10 +1202,11 @@ { "pattern": [{ "protocol": "http", "port": "80 " }], "inputs": [{ "protocol": "http", "port": "80" }], - "exactly_empty_components": ["port"], - "expected_match": { - "protocol": { "input": "http", "groups": {} } - } + "expected_obj": { + "protocol": "http", + "port": "80" + }, + "expected_match": null }, { "pattern": [{ "protocol": "http", "port": "100000" }], @@ -1229,6 +1230,34 @@ "port": { "input": "80", "groups": {}} } }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "8\t0" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "80x" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "80?x" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "80\\x" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, { "pattern": [{ "port": "(.*)" }], "inputs": [{ "port": "invalid80" }], @@ -1874,7 +1903,17 @@ { "pattern": [ "https://{sub.}?example{.com/}foo" ], "inputs": [ "https://example.com/foo" ], - "expected_obj": "error" + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "{sub.}?example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": { "0": "/foo" } } + } }, { "pattern": [ "{https://}example.com/foo" ], @@ -2952,5 +2991,122 @@ "pattern": [{ "pathname": "/([\\d&&[0-1]])" }], "inputs": [{ "pathname": "/3" }], "expected_match": null + }, + { + "pattern": [{ "protocol": "http", "hostname": "example.com/ignoredpath" }], + "inputs": ["http://example.com/"], + "expected_obj": { + "protocol": "http", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [{ "protocol": "http", "hostname": "example.com\\?ignoredsearch" }], + "inputs": ["http://example.com/"], + "expected_obj": { + "protocol": "http", + "hostname": "example.com", + "search": "*" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [{ "protocol": "http", "hostname": "example.com#ignoredhash" }], + "inputs": ["http://example.com/"], + "expected_obj": { + "protocol": "http", + "hostname": "example.com", + "hash": "*" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/x"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/x", "groups": { "0": "x" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/xyz"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/xyz", "groups": { "0": "xyz" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/example"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/example", "groups": { "0": "example" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/text"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/text", "groups": { "0": "text" } } + } + }, + { + "pattern": ["https://www.example.com/*"], + "inputs": ["https://www.example.com/path/with/x"], + "exactly_empty_components": ["port"], + "expected_obj": { + "protocol": "https", + "hostname": "www.example.com", + "pathname": "/*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "www.example.com", "groups": {} }, + "pathname": { "input": "/path/with/x", "groups": { "0": "path/with/x" } } + } } ] \ No newline at end of file From 4aea563b09add3fd5a99014099f7750a5512cbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 26 Nov 2025 02:08:10 +0000 Subject: [PATCH 03/98] Rename urlpattern dependency to deno_urlpattern in Cargo.toml --- Cargo.toml | 2 +- src/lib.rs | 135 +++++++++++++++++++++++++++-------------------------- 2 files changed, 70 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 648cb65..b4725b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,4 @@ crate-type = ["cdylib"] pyo3 = "0.27.1" regex = "1.12.2" url = "2.5.7" -urlpattern = "0.4.1" +deno_urlpattern = { package = "urlpattern", version = "0.4.1" } diff --git a/src/lib.rs b/src/lib.rs index 171807b..6ef845b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,32 +3,34 @@ use std::collections::HashMap; use pyo3::{ + BoundObject, exceptions::PyValueError, prelude::*, types::{PyDict, PyList}, - BoundObject, }; -use urlpattern::UrlPatternOptions; -#[pyclass] -pub struct URLPattern(pub urlpattern::UrlPattern); +#[pyclass(name = "URLPattern")] +pub struct UrlPattern(pub deno_urlpattern::UrlPattern); #[pymethods] -impl URLPattern { +impl UrlPattern { #[new] #[pyo3(signature = (input=None, baseURL=None))] - pub fn new(input: Option, baseURL: Option<&str>) -> PyResult { + pub fn new(input: Option, baseURL: Option<&str>) -> PyResult { let string_or_init_input = match input { - Some(input) => urlpattern::quirks::StringOrInit::try_from(input)?, - None => { - urlpattern::quirks::StringOrInit::Init(urlpattern::quirks::UrlPatternInit::default()) - } + Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, + None => deno_urlpattern::quirks::StringOrInit::Init( + deno_urlpattern::quirks::UrlPatternInit::default(), + ), }; - Ok(URLPattern( - ::parse( - urlpattern::quirks::process_construct_pattern_input(string_or_init_input, baseURL) - .map_err(Error)?, - UrlPatternOptions::default(), + Ok(UrlPattern( + ::parse( + deno_urlpattern::quirks::process_construct_pattern_input( + string_or_init_input, + baseURL, + ) + .map_err(Error)?, + deno_urlpattern::UrlPatternOptions::default(), ) .map_err(Error)?, )) @@ -48,15 +50,15 @@ impl URLPattern { } #[pyo3(signature = (input=None, baseURL=None))] - pub fn test(&self, input: Option, baseURL: Option<&str>) -> PyResult { + pub fn test(&self, input: Option, baseURL: Option<&str>) -> PyResult { let string_or_init_input = match input { - Some(input) => urlpattern::quirks::StringOrInit::try_from(input)?, - None => { - urlpattern::quirks::StringOrInit::Init(urlpattern::quirks::UrlPatternInit::default()) - } + Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, + None => deno_urlpattern::quirks::StringOrInit::Init( + deno_urlpattern::quirks::UrlPatternInit::default(), + ), }; let Some((match_input, _)) = - urlpattern::quirks::process_match_input(string_or_init_input, baseURL) + deno_urlpattern::quirks::process_match_input(string_or_init_input, baseURL) .map_err(Error)? else { return Ok(false); @@ -67,17 +69,17 @@ impl URLPattern { #[pyo3(signature = (input=None, baseURL=None))] pub fn exec( &self, - input: Option, + input: Option, baseURL: Option<&str>, - ) -> PyResult> { + ) -> PyResult> { let string_or_init_input = match input { - Some(input) => urlpattern::quirks::StringOrInit::try_from(input)?, - None => { - urlpattern::quirks::StringOrInit::Init(urlpattern::quirks::UrlPatternInit::default()) - } + Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, + None => deno_urlpattern::quirks::StringOrInit::Init( + deno_urlpattern::quirks::UrlPatternInit::default(), + ), }; let Some((match_input, inputs)) = - urlpattern::quirks::process_match_input(string_or_init_input, baseURL) + deno_urlpattern::quirks::process_match_input(string_or_init_input, baseURL) .map_err(Error)? else { return Ok(None); @@ -86,37 +88,37 @@ impl URLPattern { return Ok(None); }; - Ok(Some(URLPatternResult { + Ok(Some(UrlPatternResult { inputs, - protocol: URLPatternComponentResult { + protocol: UrlPatternComponentResult { input: result.protocol.input, groups: result.protocol.groups, }, - username: URLPatternComponentResult { + username: UrlPatternComponentResult { input: result.username.input, groups: result.username.groups, }, - password: URLPatternComponentResult { + password: UrlPatternComponentResult { input: result.password.input, groups: result.password.groups, }, - hostname: URLPatternComponentResult { + hostname: UrlPatternComponentResult { input: result.hostname.input, groups: result.hostname.groups, }, - port: URLPatternComponentResult { + port: UrlPatternComponentResult { input: result.port.input, groups: result.port.groups, }, - pathname: URLPatternComponentResult { + pathname: UrlPatternComponentResult { input: result.pathname.input, groups: result.pathname.groups, }, - search: URLPatternComponentResult { + search: UrlPatternComponentResult { input: result.search.input, groups: result.search.groups, }, - hash: URLPatternComponentResult { + hash: UrlPatternComponentResult { input: result.hash.input, groups: result.hash.groups, }, @@ -165,19 +167,21 @@ impl URLPattern { } #[derive(FromPyObject)] -pub enum URLPatternInput<'py> { +pub enum UrlPatternInput<'py> { String(String), Init(Bound<'py, PyDict>), } -impl<'py> TryFrom> for urlpattern::quirks::StringOrInit { +impl<'py> TryFrom> for deno_urlpattern::quirks::StringOrInit { type Error = pyo3::PyErr; - fn try_from(input: URLPatternInput<'py>) -> Result { + fn try_from(input: UrlPatternInput<'py>) -> Result { Ok(match input { - URLPatternInput::String(pattern) => urlpattern::quirks::StringOrInit::String(pattern), - URLPatternInput::Init(init) => { - urlpattern::quirks::StringOrInit::Init(urlpattern::quirks::UrlPatternInit { + UrlPatternInput::String(pattern) => { + deno_urlpattern::quirks::StringOrInit::String(pattern) + } + UrlPatternInput::Init(init) => deno_urlpattern::quirks::StringOrInit::Init( + deno_urlpattern::quirks::UrlPatternInit { protocol: init .get_item("protocol")? .map(|v| v.extract::()) @@ -214,25 +218,25 @@ impl<'py> TryFrom> for urlpattern::quirks::StringOrInit { .get_item("baseURL")? .map(|v| v.extract::()) .transpose()?, - }) - } + }, + ), }) } } -pub struct URLPatternResult { - pub inputs: (urlpattern::quirks::StringOrInit, Option), - pub protocol: URLPatternComponentResult, - pub username: URLPatternComponentResult, - pub password: URLPatternComponentResult, - pub hostname: URLPatternComponentResult, - pub port: URLPatternComponentResult, - pub pathname: URLPatternComponentResult, - pub search: URLPatternComponentResult, - pub hash: URLPatternComponentResult, +pub struct UrlPatternResult { + pub inputs: (deno_urlpattern::quirks::StringOrInit, Option), + pub protocol: UrlPatternComponentResult, + pub username: UrlPatternComponentResult, + pub password: UrlPatternComponentResult, + pub hostname: UrlPatternComponentResult, + pub port: UrlPatternComponentResult, + pub pathname: UrlPatternComponentResult, + pub search: UrlPatternComponentResult, + pub hash: UrlPatternComponentResult, } -impl<'py> IntoPyObject<'py> for URLPatternResult { +impl<'py> IntoPyObject<'py> for UrlPatternResult { type Target = PyDict; type Output = Bound<'py, Self::Target>; type Error = std::convert::Infallible; @@ -244,10 +248,10 @@ impl<'py> IntoPyObject<'py> for URLPatternResult { let list = PyList::empty(py); match string_or_init { - urlpattern::quirks::StringOrInit::String(string) => { + deno_urlpattern::quirks::StringOrInit::String(string) => { list.append(string).unwrap(); } - urlpattern::quirks::StringOrInit::Init(init) => { + deno_urlpattern::quirks::StringOrInit::Init(init) => { let init_dict = PyDict::new(py); if let Some(protocol) = init.protocol { init_dict.set_item("protocol", protocol).unwrap(); @@ -300,12 +304,12 @@ impl<'py> IntoPyObject<'py> for URLPatternResult { } #[derive(IntoPyObject, IntoPyObjectRef)] -pub struct URLPatternComponentResult { +pub struct UrlPatternComponentResult { input: String, groups: HashMap>, } -pub struct Error(urlpattern::Error); +pub struct Error(deno_urlpattern::Error); impl From for PyErr { fn from(error: Error) -> Self { @@ -313,16 +317,15 @@ impl From for PyErr { } } -impl From for Error { - fn from(other: urlpattern::Error) -> Self { +impl From for Error { + fn from(other: deno_urlpattern::Error) -> Self { Self(other) } } /// A Python module implemented in Rust. #[pymodule] -#[pyo3(name = "urlpattern")] -pub fn python_urlpattern(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - Ok(()) +mod urlpattern { + #[pymodule_export] + use super::UrlPattern; } From 0b9776234d03acf9f97c7e3f8c2ba6fff994da71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 26 Nov 2025 02:15:07 +0000 Subject: [PATCH 04/98] Remove unnecessary VS Code settings --- .vscode/settings.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8b30857..9b38853 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,11 +3,5 @@ "tests" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "python.analysis.typeCheckingMode": "basic", - "python.analysis.autoImportCompletions": true, - "python.analysis.inlayHints.callArgumentNames": "partial", - "python.analysis.inlayHints.functionReturnTypes": true, - "python.analysis.inlayHints.pytestParameters": true, - "python.analysis.inlayHints.variableTypes": true + "python.testing.pytestEnabled": true } \ No newline at end of file From 81ae5704295d022aa40546d1e18c492e7d6ca079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 26 Nov 2025 02:28:26 +0000 Subject: [PATCH 05/98] Remove unnecessary xfail condition for bad hostname --- tests/test_lib.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_lib.py b/tests/test_lib.py index 65848cc..c7a5ede 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -24,12 +24,6 @@ def test(entry): pytest.xfail("unsupported parameter") if entry.get("expected_obj") == "error": - if ( - isinstance(entry["pattern"][0], dict) - and entry["pattern"][0].get("hostname") == "bad\\:hostname" - ): - pytest.xfail("unknown") - with pytest.raises(Exception): URLPattern(*entry["pattern"]) return From dac182d60cee60926031a371f7c7769ee9dc378a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 26 Nov 2025 02:29:41 +0000 Subject: [PATCH 06/98] Use UnicodeEncodeError instead of ValueError --- tests/test_lib.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_lib.py b/tests/test_lib.py index c7a5ede..3286aa5 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -31,8 +31,10 @@ def test(entry): try: pattern = URLPattern(*entry["pattern"]) - except ValueError: - pytest.xfail("unsupported regular expression") + except UnicodeEncodeError as e: + if e.reason == "surrogates not allowed": + pytest.xfail(e.reason) + raise if "expected_obj" in entry: for key in entry["expected_obj"]: From 09071cff28ca1659bc0becb7c77250335f02754d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 26 Nov 2025 02:42:15 +0000 Subject: [PATCH 07/98] Refactor --- tests/test_lib.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_lib.py b/tests/test_lib.py index 3286aa5..efcdfab 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -2,6 +2,7 @@ import pathlib import pytest + from urlpattern import URLPattern # This test is based on the web-platform-tests Project. @@ -11,8 +12,9 @@ # 1. Go to https://github.com/web-platform-tests/wpt/blob/master/urlpattern/resources/urlpatterntestdata.json. # 2. Copy the content. # 3. Paste into `tests/urlpatterntestdata.json`. -urlpatterntestdata_path = pathlib.Path("tests/urlpatterntestdata.json") -urlpatterntestdata = json.loads(urlpatterntestdata_path.read_text("utf-8")) +urlpatterntestdata = json.loads( + pathlib.Path("tests/urlpatterntestdata.json").read_text("utf-8") +) @pytest.mark.parametrize("entry", urlpatterntestdata) From 3068f03b3025d0be95a96ea35bcd423b64dd4a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 26 Nov 2025 03:14:28 +0000 Subject: [PATCH 08/98] Update uraimo/run-on-arch-action to v3 --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1d062a5..70a7ffd 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -65,7 +65,7 @@ jobs: pytest - name: pytest if: ${{ !startsWith(matrix.platform.target, 'x86') && matrix.platform.target != 'ppc64' }} - uses: uraimo/run-on-arch-action@v2 + uses: uraimo/run-on-arch-action@v3 with: arch: ${{ matrix.platform.target }} distro: ubuntu22.04 From 77a3588abc679030755f790eae4f7765dc0dabd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 26 Nov 2025 03:28:11 +0000 Subject: [PATCH 09/98] Revert "Update uraimo/run-on-arch-action to v3" This reverts commit 3068f03b3025d0be95a96ea35bcd423b64dd4a9c. --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 70a7ffd..1d062a5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -65,7 +65,7 @@ jobs: pytest - name: pytest if: ${{ !startsWith(matrix.platform.target, 'x86') && matrix.platform.target != 'ppc64' }} - uses: uraimo/run-on-arch-action@v3 + uses: uraimo/run-on-arch-action@v2 with: arch: ${{ matrix.platform.target }} distro: ubuntu22.04 From 2a8e4175a4ab42466a8eed0d7d8a05ed234ab714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 3 Dec 2025 05:11:10 +0000 Subject: [PATCH 10/98] Update pyo3 --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa64983..e359ffc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,9 +219,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf" +checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" dependencies = [ "indoc", "libc", @@ -236,18 +236,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb" +checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be" +checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" dependencies = [ "libc", "pyo3-build-config", @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71" +checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b" +checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index b4725b5..c3dfa49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ categories = ["web-programming"] crate-type = ["cdylib"] [dependencies] -pyo3 = "0.27.1" +pyo3 = "0.27.2" regex = "1.12.2" url = "2.5.7" deno_urlpattern = { package = "urlpattern", version = "0.4.1" } From 782d7c2ebc1a472587464bacb1289160a13aaeae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 3 Dec 2025 05:13:03 +0000 Subject: [PATCH 11/98] Support additional parameter patterns --- src/lib.rs | 39 ++++++++++++++++++++++++++++++++++----- tests/test_lib.py | 34 ++++++++++++---------------------- urlpattern.pyi | 14 ++++++++++++-- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6ef845b..d50f5bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,27 +10,56 @@ use pyo3::{ }; #[pyclass(name = "URLPattern")] -pub struct UrlPattern(pub deno_urlpattern::UrlPattern); +struct UrlPattern(deno_urlpattern::UrlPattern); #[pymethods] impl UrlPattern { #[new] - #[pyo3(signature = (input=None, baseURL=None))] - pub fn new(input: Option, baseURL: Option<&str>) -> PyResult { + #[pyo3(signature = (input=None, baseURL=None, options=None))] + pub fn new( + input: Option, + baseURL: Option<&Bound<'_, PyAny>>, + options: Option<&Bound<'_, PyDict>>, + ) -> PyResult { + let (base_url, options) = match baseURL { + Some(value) => { + if let Ok(options_dict) = value.cast::() { + (None, Some(options_dict)) + } else if value.is_none() { + (None, options) + } else { + (Some(value.extract::()?), options) + } + } + None => (None, options), + }; + let string_or_init_input = match input { Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, None => deno_urlpattern::quirks::StringOrInit::Init( deno_urlpattern::quirks::UrlPatternInit::default(), ), }; + let options = if let Some(options) = options { + deno_urlpattern::UrlPatternOptions { + ignore_case: options + .get_item("ignoreCase")? + .map(|v| v.extract::()) + .transpose()? + .unwrap_or(false), + ..deno_urlpattern::UrlPatternOptions::default() + } + } else { + deno_urlpattern::UrlPatternOptions::default() + }; Ok(UrlPattern( ::parse( deno_urlpattern::quirks::process_construct_pattern_input( string_or_init_input, - baseURL, + base_url.as_deref(), ) .map_err(Error)?, - deno_urlpattern::UrlPatternOptions::default(), + options, ) .map_err(Error)?, )) diff --git a/tests/test_lib.py b/tests/test_lib.py index efcdfab..6f8b2c0 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -19,11 +19,8 @@ @pytest.mark.parametrize("entry", urlpatterntestdata) def test(entry): - if len(entry["pattern"]) == 2 and isinstance(entry["pattern"][1], dict): - pytest.xfail("unsupported parameter") - - if len(entry["pattern"]) == 3 and isinstance(entry["pattern"][2], dict): - pytest.xfail("unsupported parameter") + if entry["pattern"] == [{"pathname": "*{}**?"}]: + pytest.skip("unsupported in the implementation") if entry.get("expected_obj") == "error": with pytest.raises(Exception): @@ -35,35 +32,28 @@ def test(entry): except UnicodeEncodeError as e: if e.reason == "surrogates not allowed": - pytest.xfail(e.reason) + pytest.skip("unsupported in the implementation") raise if "expected_obj" in entry: for key in entry["expected_obj"]: - if getattr(pattern, key) in ("", "/") and entry["expected_obj"][key] == "*": - assert True - - else: - assert getattr(pattern, key) == entry["expected_obj"][key] + assert getattr(pattern, key) == entry["expected_obj"][key] if entry.get("expected_match") == "error": with pytest.raises(Exception): pattern.exec(*entry["inputs"]) return - elif isinstance(entry.get("expected_match"), dict): + if isinstance(entry.get("expected_match"), dict): result = pattern.exec(*entry["inputs"]) + assert result for key in entry["expected_match"]: - if key != "inputs": - for group in entry["expected_match"][key]["groups"]: - if entry["expected_match"][key]["groups"][group] is None: - pytest.xfail("unsupported undefined group") + assert result[key] == entry["expected_match"][key] - if "exactly_empty_components" in entry: - if key not in entry["exactly_empty_components"]: - continue + if "exactly_empty_components" in entry: + result = pattern.exec(*entry["inputs"]) - else: - assert result - assert result[key] == entry["expected_match"][key] + for component in entry["exactly_empty_components"]: + if result: + assert result[component]["groups"] == {} \ No newline at end of file diff --git a/urlpattern.pyi b/urlpattern.pyi index 7d85965..1e2e266 100644 --- a/urlpattern.pyi +++ b/urlpattern.pyi @@ -2,11 +2,21 @@ from typing_extensions import TypeAlias, TypedDict, overload URLPatternInput: TypeAlias = str | URLPatternInit +class URLPatternOptions(TypedDict, total=False): + ignoreCase: bool + class URLPattern: @overload - def __init__(self, input: URLPatternInput, baseURL: str): ... + def __init__( + self, + input: URLPatternInput, + baseURL: str, + options: URLPatternOptions | None = None, + ): ... @overload - def __init__(self, input: URLPatternInput = {}): ... + def __init__( + self, input: URLPatternInput, options: URLPatternOptions | None = None + ): ... def test(self, input: URLPatternInput = {}, baseURL: str | None = None) -> bool: ... def exec( self, input: URLPatternInput = {}, baseURL: str | None = None From 400fcf771954a294bd7bfc1c3a304f9d64fab773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 3 Dec 2025 23:31:12 +0000 Subject: [PATCH 12/98] Downgrade urlpattern --- Cargo.lock | 51 ++++++++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 2 +- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e359ffc..d2f8b3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "litemap" @@ -412,6 +412,47 @@ dependencies = [ "zerovec", ] +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -438,13 +479,13 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.4.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957a88ad1abd5d13336275adb17d4f9b6a2404f3baed2e075e0b026dc0b2b58d" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" dependencies = [ - "icu_properties", "regex", "serde", + "unic-ucd-ident", "url", ] diff --git a/Cargo.toml b/Cargo.toml index c3dfa49..b25906f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,4 @@ crate-type = ["cdylib"] pyo3 = "0.27.2" regex = "1.12.2" url = "2.5.7" -deno_urlpattern = { package = "urlpattern", version = "0.4.1" } +deno_urlpattern = { package = "urlpattern", version = "0.3.0" } From 6a4012eb9dbe04ac0a02122973544d5885b15884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 3 Dec 2025 23:39:45 +0000 Subject: [PATCH 13/98] Revert dependency versions --- Cargo.lock | 75 +++++++++++++----------------------------------------- Cargo.toml | 8 +++--- 2 files changed, 21 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2f8b3f..f4d7725 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "litemap" @@ -219,9 +219,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.27.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" +checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf" dependencies = [ "indoc", "libc", @@ -236,18 +236,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.27.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" +checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.27.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" +checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be" dependencies = [ "libc", "pyo3-build-config", @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.27.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" +checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.27.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" +checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b" dependencies = [ "heck", "proc-macro2", @@ -376,9 +376,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -412,47 +412,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicode-ident" version = "1.0.22" @@ -479,13 +438,13 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +checksum = "957a88ad1abd5d13336275adb17d4f9b6a2404f3baed2e075e0b026dc0b2b58d" dependencies = [ + "icu_properties", "regex", "serde", - "unic-ucd-ident", "url", ] diff --git a/Cargo.toml b/Cargo.toml index b25906f..5ffbc1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ categories = ["web-programming"] crate-type = ["cdylib"] [dependencies] -pyo3 = "0.27.2" -regex = "1.12.2" -url = "2.5.7" -deno_urlpattern = { package = "urlpattern", version = "0.3.0" } +pyo3 = "0.27.1" +regex = "1.11.0" +url = "2.5.0" +deno_urlpattern = { package = "urlpattern", version = "0.4.1" } From 2b5070e7d52e0776305e675814459579755cece2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Fri, 5 Dec 2025 00:28:38 +0000 Subject: [PATCH 14/98] Update PyO3 --- Cargo.lock | 30 ++++++++++++++---------------- Cargo.toml | 4 +--- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4d7725..f9e5a04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "litemap" @@ -219,9 +219,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf" +checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" dependencies = [ "indoc", "libc", @@ -236,18 +236,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb" +checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be" +checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" dependencies = [ "libc", "pyo3-build-config", @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71" +checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b" +checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" dependencies = [ "heck", "proc-macro2", @@ -283,8 +283,6 @@ name = "python-urlpattern" version = "0.1.4" dependencies = [ "pyo3", - "regex", - "url", "urlpattern", ] @@ -376,9 +374,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5ffbc1f..ffa89cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,5 @@ categories = ["web-programming"] crate-type = ["cdylib"] [dependencies] -pyo3 = "0.27.1" -regex = "1.11.0" -url = "2.5.0" +pyo3 = "0.27.2" deno_urlpattern = { package = "urlpattern", version = "0.4.1" } From 1f282d40bef3b138856685e8eb7e006accfc304a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Fri, 5 Dec 2025 00:56:47 +0000 Subject: [PATCH 15/98] Bump version to 0.1.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9e5a04..b8a2a95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,7 +280,7 @@ dependencies = [ [[package]] name = "python-urlpattern" -version = "0.1.4" +version = "0.1.5" dependencies = [ "pyo3", "urlpattern", diff --git a/Cargo.toml b/Cargo.toml index ffa89cc..ec2f549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python-urlpattern" -version = "0.1.4" +version = "0.1.5" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" description = "An implementation of the URL Pattern Standard for Python written in Rust" From 25b8a32257a1d8042c46b921b3c4e9118785228d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Fri, 5 Dec 2025 10:26:27 +0900 Subject: [PATCH 16/98] Update Python image version in devcontainer.json --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1dedac9..1eaee6b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "name": "Python 3", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", + "image": "mcr.microsoft.com/devcontainers/python:2-3.14-trixie", // Features to add to the dev container. More info: https://containers.dev/features. "features": { From 9c5f9a6f556b401c7203f64db81ef8ac3a93213a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 8 Dec 2025 01:03:22 +0000 Subject: [PATCH 17/98] Set .venv as the default Python interpreter --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9b38853..201a6d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { + "python.defaultInterpreterPath": "${workspaceFolder}/.venv", "python.testing.pytestArgs": [ "tests" ], From 7634d62e0527138b35851d76103ce702924ecf4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 8 Dec 2025 04:25:32 +0000 Subject: [PATCH 18/98] Update postCreateCommand in devcontainer.json for improved environment setup --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1eaee6b..15c8e79 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,7 +14,7 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "pip3 install --user maturin pytest && python3 -m venv .venv" + "postCreateCommand": "python -m venv .venv && .venv/bin/pip install pytest && pipx install maturin && maturin develop" // Configure tool-specific properties. // "customizations": {}, From 54efd1fe4bb847fd859de4ccf99e5576318d43e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 8 Dec 2025 04:27:01 +0000 Subject: [PATCH 19/98] Update image version in devcontainer.json --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 15c8e79..ab092dc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "name": "Python 3", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/python:2-3.14-trixie", + "image": "mcr.microsoft.com/devcontainers/python:3-3.14-trixie", // Features to add to the dev container. More info: https://containers.dev/features. "features": { From 571569cb8d3e993e3ceff14587f9dc801de96711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 8 Dec 2025 05:22:38 +0000 Subject: [PATCH 20/98] Update README.md --- Cargo.toml | 2 +- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++-------- pyproject.toml | 2 +- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ec2f549..d2fd6a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "python-urlpattern" version = "0.1.5" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" -description = "An implementation of the URL Pattern Standard for Python written in Rust" +description = "An implementation of the URL Pattern Standard for Python written in Rust." repository = "https://github.com/urlpattern/python-urlpattern" license = "MIT" keywords = ["urlpattern"] diff --git a/README.md b/README.md index 277afd6..46f32f9 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,38 @@ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![CI](https://github.com/urlpattern/python-urlpattern/actions/workflows/CI.yml/badge.svg)](https://github.com/urlpattern/python-urlpattern/actions) -An implementation of [the URL Pattern Standard](https://urlpattern.spec.whatwg.org/) for Python written in Rust +An implementation of [the URL Pattern Standard](https://urlpattern.spec.whatwg.org/) for Python written in Rust. ## Introduction -It provides a pattern matching syntax like `/users/:id/`, similar to [Express](https://expressjs.com/) or [Path-to-RegExp](https://github.com/pillarjs/path-to-regexp) in Node.js. You can use it as a foundation to build your own web server or framework. +The URL Pattern Standard is a web standard for URL pattern matching. It is useful on the server side when serving different pages based on the URL. It provides pattern matching syntax like `/users/:id`, similar to [route parameters in Express](https://expressjs.com/en/guide/routing.html#route-parameters) or [Path-to-RegExp](https://github.com/pillarjs/path-to-regexp). You can use it as a foundation to build your own web server or framework. It's a thin wrapper of [denoland/rust-urlpattern](https://github.com/denoland/rust-urlpattern) with [PyO3](https://github.com/PyO3/pyo3) + [Maturin](https://github.com/PyO3/maturin). +## Installation + +On Linux/UNIX or macOS: + +```sh +pip install urlpattern +``` + +On Windows: + +```sh +py -m pip install urlpattern +``` + +## Usage + +Check [urlpattern.pyi](urlpattern.pyi). + ## Examples +For various usage examples, refer to [Chrome for Developers](https://developer.chrome.com/docs/web-platform/urlpattern) or [MDN](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) (you may need to convert JavaScript into Python). + +### `test` + ```py from urlpattern import URLPattern @@ -23,6 +45,8 @@ print(pattern.test("https://example.com/admin/main/")) # output: True print(pattern.test("https://example.com/main/")) # output: False ``` +### `exec` + ```py from urlpattern import URLPattern @@ -31,22 +55,45 @@ result = pattern.exec({"pathname": "/users/4163/"}) print(result["pathname"]["groups"]["id"]) # output: 4163 ``` -## Installation +### `ignoreCase` -On Linux/UNIX or macOS: +```py +from urlpattern import URLPattern -```sh -pip install urlpattern +pattern = URLPattern("https://example.com/test") +print(pattern.test("https://example.com/test")) # output: True +print(pattern.test("https://example.com/TeST")) # output: False + +pattern = URLPattern("https://example.com/test", {"ignoreCase": True}) +print(pattern.test("https://example.com/test")) # output: True +print(pattern.test("https://example.com/TeST")) # output: True ``` -On Windows: +### Simple WSGI app -```sh -py -m pip install urlpattern +```py +from urlpattern import URLPattern +from wsgiref.simple_server import make_server + + +user_id_pattern = URLPattern({"pathname": "/users/:id"}) + + +def app(environ, start_response): + path = environ["PATH_INFO"] + + if result := user_id_pattern.exec({"pathname": path}): + user_id = result["pathname"]["groups"]["id"] + start_response("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + return [f"{user_id=}".encode()] + + +with make_server("", 8000, app) as httpd: + httpd.serve_forever() ``` ## Limitations Due to limitations in the dependency [denoland/rust-urlpattern](https://github.com/denoland/rust-urlpattern), it may not support all features specified in [the standard](https://urlpattern.spec.whatwg.org/). -Check the limitations in [`tests/test_lib.py`](tests/test_lib.py). +Check the `pytest.skip` in [`tests/test_lib.py`](tests/test_lib.py). diff --git a/pyproject.toml b/pyproject.toml index b423a52..0639a66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "urlpattern" -description = "An implementation of the URL Pattern Standard for Python written in Rust" +description = "An implementation of the URL Pattern Standard for Python written in Rust." readme = "README.md" requires-python = ">=3.8" authors = [{ name = "방성범 (Bang Seongbeom)", email = "bangseongbeom@gmail.com" }] From 261e169f3ab36be686d230dfa7ad07b354a1e9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 8 Dec 2025 15:45:06 +0000 Subject: [PATCH 21/98] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 46f32f9..d654248 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ An implementation of [the URL Pattern Standard](https://urlpattern.spec.whatwg.o ## Introduction -The URL Pattern Standard is a web standard for URL pattern matching. It is useful on the server side when serving different pages based on the URL. It provides pattern matching syntax like `/users/:id`, similar to [route parameters in Express](https://expressjs.com/en/guide/routing.html#route-parameters) or [Path-to-RegExp](https://github.com/pillarjs/path-to-regexp). You can use it as a foundation to build your own web server or framework. +The URL Pattern Standard is a web standard for URL pattern matching. It is useful on the server side when serving different pages based on the URL (a.k.a. routing). It provides pattern matching syntax like `/users/:id`, similar to [route parameters in Express](https://expressjs.com/en/guide/routing.html#route-parameters) or [Path-to-RegExp](https://github.com/pillarjs/path-to-regexp). You can use it as a foundation to build your own web server or framework. It's a thin wrapper of [denoland/rust-urlpattern](https://github.com/denoland/rust-urlpattern) with [PyO3](https://github.com/PyO3/pyo3) + [Maturin](https://github.com/PyO3/maturin). @@ -29,11 +29,11 @@ py -m pip install urlpattern ## Usage -Check [urlpattern.pyi](urlpattern.pyi). +Check [urlpattern.pyi](https://github.com/urlpattern/python-urlpattern/blob/main/urlpattern.pyi). ## Examples -For various usage examples, refer to [Chrome for Developers](https://developer.chrome.com/docs/web-platform/urlpattern) or [MDN](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) (you may need to convert JavaScript into Python). +For various usage examples, refer to [Chrome for Developers](https://developer.chrome.com/docs/web-platform/urlpattern) or [MDN](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) (you may need to convert JavaScript into Python). ### `test` @@ -96,4 +96,4 @@ with make_server("", 8000, app) as httpd: Due to limitations in the dependency [denoland/rust-urlpattern](https://github.com/denoland/rust-urlpattern), it may not support all features specified in [the standard](https://urlpattern.spec.whatwg.org/). -Check the `pytest.skip` in [`tests/test_lib.py`](tests/test_lib.py). +Check the `pytest.skip` in [`tests/test_lib.py`](https://github.com/urlpattern/python-urlpattern/blob/main/tests/test_lib.py). From 708f61cc46f43e19444f8456680edc8f8263e921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 8 Dec 2025 15:55:04 +0000 Subject: [PATCH 22/98] Update macOS runners in CI.yml --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1d062a5..2880a21 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -182,9 +182,9 @@ jobs: strategy: matrix: platform: - - runner: macos-13 + - runner: macos-15-intel target: x86_64 - - runner: macos-14 + - runner: macos-latest target: aarch64 steps: - uses: actions/checkout@v4 From d25bcb830f83542276a3964994eed78b0bbb3da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 29 Dec 2025 03:30:59 +0900 Subject: [PATCH 23/98] Enhance README examples --- README.md | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d654248..58cefd4 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,11 @@ py -m pip install urlpattern ## Usage -Check [urlpattern.pyi](https://github.com/urlpattern/python-urlpattern/blob/main/urlpattern.pyi). +Check [urlpattern.pyi](https://github.com/urlpattern/python-urlpattern/blob/main/urlpattern.pyi) or the examples below. -## Examples +For various usage examples, you can also check [Chrome for Developers](https://developer.chrome.com/docs/web-platform/urlpattern) or [MDN](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) (you need to convert JavaScript into Python). -For various usage examples, refer to [Chrome for Developers](https://developer.chrome.com/docs/web-platform/urlpattern) or [MDN](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) (you may need to convert JavaScript into Python). +## Examples ### `test` @@ -72,20 +72,33 @@ print(pattern.test("https://example.com/TeST")) # output: True ### Simple WSGI app ```py -from urlpattern import URLPattern from wsgiref.simple_server import make_server +from urlpattern import URLPattern user_id_pattern = URLPattern({"pathname": "/users/:id"}) +def get_user_id(environ, start_response): + user_id = environ["result"]["pathname"]["groups"]["id"] + status = "200 OK" + response_headers = [("Content-type", "text/plain; charset=utf-8")] + start_response(status, response_headers) + return [f"{user_id=}".encode()] + + def app(environ, start_response): path = environ["PATH_INFO"] + method = environ["REQUEST_METHOD"] if result := user_id_pattern.exec({"pathname": path}): - user_id = result["pathname"]["groups"]["id"] - start_response("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) - return [f"{user_id=}".encode()] + if method == "GET": + return get_user_id(environ | {"result": result}, start_response) + + status = "404 Not Found" + response_headers = [("Content-type", "text/plain; charset=utf-8")] + start_response(status, response_headers) + return [b"Not Found"] with make_server("", 8000, app) as httpd: @@ -96,4 +109,4 @@ with make_server("", 8000, app) as httpd: Due to limitations in the dependency [denoland/rust-urlpattern](https://github.com/denoland/rust-urlpattern), it may not support all features specified in [the standard](https://urlpattern.spec.whatwg.org/). -Check the `pytest.skip` in [`tests/test_lib.py`](https://github.com/urlpattern/python-urlpattern/blob/main/tests/test_lib.py). +Check `pytest.skip` in [`tests/test_lib.py`](https://github.com/urlpattern/python-urlpattern/blob/main/tests/test_lib.py). From d3bc43872ce10334f07f7f9179556c2a63de3d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 28 Dec 2025 18:40:47 +0000 Subject: [PATCH 24/98] Update dependencies --- Cargo.lock | 26 +++++++++++++------------- Cargo.toml | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8a2a95..287a1aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,9 +91,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -105,9 +105,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -195,9 +195,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "potential_utf" @@ -210,9 +210,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -280,7 +280,7 @@ dependencies = [ [[package]] name = "python-urlpattern" -version = "0.1.5" +version = "0.1.6" dependencies = [ "pyo3", "urlpattern", @@ -396,9 +396,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" +checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" [[package]] name = "tinystr" @@ -436,9 +436,9 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957a88ad1abd5d13336275adb17d4f9b6a2404f3baed2e075e0b026dc0b2b58d" +checksum = "0f805818f843b548bacc19609eb3619dd2850e54746f5cada37927393c2ef4ec" dependencies = [ "icu_properties", "regex", diff --git a/Cargo.toml b/Cargo.toml index d2fd6a4..278aec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python-urlpattern" -version = "0.1.5" +version = "0.1.6" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" description = "An implementation of the URL Pattern Standard for Python written in Rust." @@ -15,4 +15,4 @@ crate-type = ["cdylib"] [dependencies] pyo3 = "0.27.2" -deno_urlpattern = { package = "urlpattern", version = "0.4.1" } +deno_urlpattern = { package = "urlpattern", version = "0.4.2" } From 5949fbc213888d35ce676b5dd74bc7ab008ddcd6 Mon Sep 17 00:00:00 2001 From: Roman Tolkachyov Date: Sun, 25 Jan 2026 23:34:13 +0300 Subject: [PATCH 25/98] Mark pymodule as supporting free-threading python (nogil) --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d50f5bd..9e6e278 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -353,7 +353,7 @@ impl From for Error { } /// A Python module implemented in Rust. -#[pymodule] +#[pymodule(gil_used = false)] mod urlpattern { #[pymodule_export] use super::UrlPattern; From 613aaefb468c3896f5858a30e2c7117844e65b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 25 Jan 2026 21:21:35 +0000 Subject: [PATCH 26/98] Upgrade Maturin --- .github/workflows/CI.yml | 52 +++++++++++++++++++++------------------- pyproject.toml | 2 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2880a21..71457b7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,4 @@ -# This file is autogenerated by maturin v1.10.2 +# This file is autogenerated by maturin v1.11.5 # To update, run # # maturin generate-ci --pytest github @@ -37,8 +37,8 @@ jobs: - runner: ubuntu-22.04 target: ppc64le steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: Build wheels @@ -49,7 +49,7 @@ jobs: sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: auto - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: wheels-linux-${{ matrix.platform.target }} path: dist @@ -93,8 +93,8 @@ jobs: - runner: ubuntu-22.04 target: armv7 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: Build wheels @@ -105,7 +105,7 @@ jobs: sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: musllinux_1_2 - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: wheels-musllinux-${{ matrix.platform.target }} path: dist @@ -147,14 +147,19 @@ jobs: platform: - runner: windows-latest target: x64 + python_arch: x64 - runner: windows-latest target: x86 + python_arch: x86 + - runner: windows-11-arm + target: aarch64 + python_arch: arm64 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: 3.x - architecture: ${{ matrix.platform.target }} + python-version: 3.13 + architecture: ${{ matrix.platform.python_arch }} - name: Build wheels uses: PyO3/maturin-action@v1 with: @@ -162,12 +167,11 @@ jobs: args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: wheels-windows-${{ matrix.platform.target }} path: dist - name: pytest - if: ${{ !startsWith(matrix.platform.target, 'aarch64') }} shell: bash run: | set -e @@ -187,8 +191,8 @@ jobs: - runner: macos-latest target: aarch64 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: Build wheels @@ -198,7 +202,7 @@ jobs: args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: wheels-macos-${{ matrix.platform.target }} path: dist @@ -214,14 +218,14 @@ jobs: sdist: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build sdist uses: PyO3/maturin-action@v1 with: command: sdist args: --out dist - name: Upload sdist - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: wheels-sdist path: dist @@ -240,14 +244,14 @@ jobs: # Used to generate artifact attestation attestations: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v6 - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v3 with: subject-path: 'wheels-*/*' + - name: Install uv + if: ${{ startsWith(github.ref, 'refs/tags/') }} + uses: astral-sh/setup-uv@v7 - name: Publish to PyPI if: ${{ startsWith(github.ref, 'refs/tags/') }} - uses: PyO3/maturin-action@v1 - with: - command: upload - args: --non-interactive --skip-existing wheels-*/* + run: uv publish 'wheels-*/*' diff --git a/pyproject.toml b/pyproject.toml index 0639a66..2f818e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=1.10,<2.0"] +requires = ["maturin>=1.11,<2.0"] build-backend = "maturin" [project] From f3ca9f1254027cec1a49051b8de8f1a2eb5aadad Mon Sep 17 00:00:00 2001 From: Roman Tolkachyov Date: Mon, 26 Jan 2026 00:23:38 +0300 Subject: [PATCH 27/98] Improve python typings for PyCharm --- urlpattern.pyi | 52 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/urlpattern.pyi b/urlpattern.pyi index 1e2e266..811ba31 100644 --- a/urlpattern.pyi +++ b/urlpattern.pyi @@ -1,25 +1,55 @@ from typing_extensions import TypeAlias, TypedDict, overload -URLPatternInput: TypeAlias = str | URLPatternInit class URLPatternOptions(TypedDict, total=False): ignoreCase: bool + +class URLPatternInit(TypedDict, total=False): + protocol: str + username: str + password: str + hostname: str + port: str + pathname: str + search: str + hash: str + baseURL: str + class URLPattern: @overload def __init__( self, - input: URLPatternInput, + input: URLPatternInit, baseURL: str, options: URLPatternOptions | None = None, ): ... @overload def __init__( - self, input: URLPatternInput, options: URLPatternOptions | None = None + self, + input: str, + baseURL: str, + options: URLPatternOptions | None = None, ): ... - def test(self, input: URLPatternInput = {}, baseURL: str | None = None) -> bool: ... + @overload + def __init__( + self, input: URLPatternInit, options: URLPatternOptions | None = None + ): ... + @overload + def __init__( + self, input: str, options: URLPatternOptions | None = None + ): ... + @overload + def test(self, input: URLPatternInit, baseURL: str | None = None) -> bool: ... + @overload + def test(self, input: str, baseURL: str | None = None) -> bool: ... + @overload def exec( - self, input: URLPatternInput = {}, baseURL: str | None = None + self, input: URLPatternInit, baseURL: str | None = None + ) -> URLPatternResult | None: ... + @overload + def exec( + self, input: str, baseURL: str | None = None ) -> URLPatternResult | None: ... @property def protocol(self) -> str: ... @@ -38,19 +68,9 @@ class URLPattern: @property def hash(self) -> str: ... -class URLPatternInit(TypedDict, total=False): - protocol: str - username: str - password: str - hostname: str - port: str - pathname: str - search: str - hash: str - baseURL: str class URLPatternResult(TypedDict): - inputs: list[URLPatternInput] + inputs: list[str | URLPatternInit] protocol: URLPatternComponentResult username: URLPatternComponentResult From 1a4cc491b81500a19a96eba90d17d5bc535fc644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 25 Jan 2026 21:35:17 +0000 Subject: [PATCH 28/98] Fix test assertion --- tests/test_lib.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_lib.py b/tests/test_lib.py index 6f8b2c0..b44c67d 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -46,14 +46,17 @@ def test(entry): if isinstance(entry.get("expected_match"), dict): result = pattern.exec(*entry["inputs"]) - assert result for key in entry["expected_match"]: assert result[key] == entry["expected_match"][key] + else: + result = pattern.exec(*entry["inputs"]) + assert result is None + if "exactly_empty_components" in entry: result = pattern.exec(*entry["inputs"]) for component in entry["exactly_empty_components"]: if result: - assert result[component]["groups"] == {} \ No newline at end of file + assert result[component]["groups"] == {} From 6b552cca2172d99a7a263c407f12d3874766f74e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 25 Jan 2026 21:35:51 +0000 Subject: [PATCH 29/98] Update urlpatterntestdata.json --- tests/urlpatterntestdata.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/urlpatterntestdata.json b/tests/urlpatterntestdata.json index 6b3fe0d..ebc171c 100644 --- a/tests/urlpatterntestdata.json +++ b/tests/urlpatterntestdata.json @@ -3108,5 +3108,15 @@ "hostname": { "input": "www.example.com", "groups": {} }, "pathname": { "input": "/path/with/x", "groups": { "0": "path/with/x" } } } + }, + { + "pattern": [{ "hostname": ":domain(.*)" }], + "inputs": [{ "hostname": "localhost" }], + "expected_obj": { + "hostname": ":domain(.*)" + }, + "expected_match": { + "hostname": { "input": "localhost", "groups": { "domain" : "localhost"} } + } } ] \ No newline at end of file From 6076998f0fcc125a0d5af167e21d2101041b0cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 25 Jan 2026 21:37:43 +0000 Subject: [PATCH 30/98] Update dependencies --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 287a1aa..630c6fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "litemap" @@ -210,9 +210,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -288,9 +288,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -374,9 +374,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -424,9 +424,9 @@ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -436,9 +436,9 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f805818f843b548bacc19609eb3619dd2850e54746f5cada37927393c2ef4ec" +checksum = "37ffe0983283c841b6c6e9f116833a0061cc690c0eb67994d65bbdfa5cd3f24e" dependencies = [ "icu_properties", "regex", diff --git a/Cargo.toml b/Cargo.toml index 278aec4..ab21225 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ crate-type = ["cdylib"] [dependencies] pyo3 = "0.27.2" -deno_urlpattern = { package = "urlpattern", version = "0.4.2" } +deno_urlpattern = { package = "urlpattern", version = "0.5.2" } From 6ade139feede479ddb32a46b1117571eed19bf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 25 Jan 2026 21:45:33 +0000 Subject: [PATCH 31/98] Bump version to 0.1.7 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 630c6fe..2b0a624 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,7 +280,7 @@ dependencies = [ [[package]] name = "python-urlpattern" -version = "0.1.6" +version = "0.1.7" dependencies = [ "pyo3", "urlpattern", diff --git a/Cargo.toml b/Cargo.toml index ab21225..3d3a647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python-urlpattern" -version = "0.1.6" +version = "0.1.7" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" description = "An implementation of the URL Pattern Standard for Python written in Rust." From a0b43b01849ef63daf3d3cc32cf498e2a1d81a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 25 Jan 2026 22:16:32 +0000 Subject: [PATCH 32/98] Update README.md --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 58cefd4..6616e5b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ The URL Pattern Standard is a web standard for URL pattern matching. It is usefu It's a thin wrapper of [denoland/rust-urlpattern](https://github.com/denoland/rust-urlpattern) with [PyO3](https://github.com/PyO3/pyo3) + [Maturin](https://github.com/PyO3/maturin). +The naming conventions follow [the standard](https://urlpattern.spec.whatwg.org/) as closely as possible, similar to [xml.dom](https://docs.python.org/3/library/xml.dom.html). + ## Installation On Linux/UNIX or macOS: @@ -55,6 +57,16 @@ result = pattern.exec({"pathname": "/users/4163/"}) print(result["pathname"]["groups"]["id"]) # output: 4163 ``` +### `baseURL` + +```py +from urlpattern import URLPattern + +pattern = URLPattern({"pathname": "/admin/*"}, "https://example.com") +print(pattern.test({"pathname": "/admin/main/"}, "https://example.com")) # output: True +print(pattern.test("/admin/main/", "https://example.com")) # output: True +``` + ### `ignoreCase` ```py @@ -69,7 +81,7 @@ print(pattern.test("https://example.com/test")) # output: True print(pattern.test("https://example.com/TeST")) # output: True ``` -### Simple WSGI app +### A simple WSGI app ```py from wsgiref.simple_server import make_server From f97725065aaabb694e5ee1e6b20de336812f762e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 26 Jan 2026 10:38:29 +0900 Subject: [PATCH 33/98] Add Free Threading in pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2f818e6..7df97b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Free Threading", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Rust", From 93d0eea46bbd81abc91e630f944d43428c40c20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 26 Jan 2026 10:41:05 +0900 Subject: [PATCH 34/98] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7df97b8..a4fdfc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -22,7 +23,6 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", - "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Free Threading", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", From a89988d1823aaa9d1e63be0c520469f9b8183993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 26 Jan 2026 10:50:21 +0900 Subject: [PATCH 35/98] Remove extension-module feature --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a4fdfc4..ce28e98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,3 @@ dynamic = ["version"] Homepage = "https://github.com/urlpattern/python-urlpattern" Repository = "https://github.com/urlpattern/python-urlpattern.git" Issues = "https://github.com/urlpattern/python-urlpattern/issues" - -[tool.maturin] -features = ["pyo3/extension-module"] From 5e17e935570484e0b85f80489a7f3883a8ae32d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 26 Jan 2026 19:17:02 +0000 Subject: [PATCH 36/98] Fix return type annotations for URLPattern constructors --- urlpattern.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/urlpattern.pyi b/urlpattern.pyi index 1e2e266..17e9bd2 100644 --- a/urlpattern.pyi +++ b/urlpattern.pyi @@ -12,11 +12,11 @@ class URLPattern: input: URLPatternInput, baseURL: str, options: URLPatternOptions | None = None, - ): ... + ) -> None: ... @overload def __init__( self, input: URLPatternInput, options: URLPatternOptions | None = None - ): ... + ) -> None: ... def test(self, input: URLPatternInput = {}, baseURL: str | None = None) -> bool: ... def exec( self, input: URLPatternInput = {}, baseURL: str | None = None From b8d8fe13184b31f085f4e76d75941256f689d80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 26 Jan 2026 19:25:31 +0000 Subject: [PATCH 37/98] Update type annotations to support Python 3.8 --- urlpattern.pyi | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/urlpattern.pyi b/urlpattern.pyi index 17e9bd2..d157457 100644 --- a/urlpattern.pyi +++ b/urlpattern.pyi @@ -1,6 +1,8 @@ -from typing_extensions import TypeAlias, TypedDict, overload +from typing import Optional, TypedDict, Union, overload -URLPatternInput: TypeAlias = str | URLPatternInit +from typing_extensions import TypeAlias + +URLPatternInput: TypeAlias = Union[str, URLPatternInit] class URLPatternOptions(TypedDict, total=False): ignoreCase: bool @@ -11,16 +13,18 @@ class URLPattern: self, input: URLPatternInput, baseURL: str, - options: URLPatternOptions | None = None, + options: Optional[URLPatternOptions] = None, ) -> None: ... @overload def __init__( - self, input: URLPatternInput, options: URLPatternOptions | None = None + self, input: URLPatternInput, options: Optional[URLPatternOptions] = None ) -> None: ... - def test(self, input: URLPatternInput = {}, baseURL: str | None = None) -> bool: ... + def test( + self, input: URLPatternInput = {}, baseURL: Optional[str] = None + ) -> bool: ... def exec( - self, input: URLPatternInput = {}, baseURL: str | None = None - ) -> URLPatternResult | None: ... + self, input: URLPatternInput = {}, baseURL: Optional[str] = None + ) -> Optional[URLPatternResult]: ... @property def protocol(self) -> str: ... @property @@ -65,4 +69,4 @@ class URLPatternComponentResult(TypedDict): input: str groups: dict[str, str] -URLPatternCompatible: TypeAlias = str | URLPatternInit | URLPattern +URLPatternCompatible: TypeAlias = Union[str, URLPatternInit, URLPattern] From 74ab0bd701df361c6de18a41db2e2bc6d66a665b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 26 Jan 2026 19:53:54 +0000 Subject: [PATCH 38/98] Make input parameter optional --- urlpattern.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/urlpattern.pyi b/urlpattern.pyi index d157457..2c9d4c1 100644 --- a/urlpattern.pyi +++ b/urlpattern.pyi @@ -17,7 +17,7 @@ class URLPattern: ) -> None: ... @overload def __init__( - self, input: URLPatternInput, options: Optional[URLPatternOptions] = None + self, input: URLPatternInput = {}, options: Optional[URLPatternOptions] = None ) -> None: ... def test( self, input: URLPatternInput = {}, baseURL: Optional[str] = None From 402b8795503fb4f90e425edca5b9bc3c1f996277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 26 Jan 2026 19:54:31 +0000 Subject: [PATCH 39/98] Change default value of options parameter to an empty dict --- urlpattern.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/urlpattern.pyi b/urlpattern.pyi index 2c9d4c1..e452c0b 100644 --- a/urlpattern.pyi +++ b/urlpattern.pyi @@ -13,11 +13,11 @@ class URLPattern: self, input: URLPatternInput, baseURL: str, - options: Optional[URLPatternOptions] = None, + options: Optional[URLPatternOptions] = {}, ) -> None: ... @overload def __init__( - self, input: URLPatternInput = {}, options: Optional[URLPatternOptions] = None + self, input: URLPatternInput = {}, options: Optional[URLPatternOptions] = {} ) -> None: ... def test( self, input: URLPatternInput = {}, baseURL: Optional[str] = None From e4f6c0554f34e4ac83cce3fa8f8975f46caf21c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Tue, 27 Jan 2026 05:10:39 +0900 Subject: [PATCH 40/98] Add missing default value --- urlpattern.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/urlpattern.pyi b/urlpattern.pyi index fd14f52..73fb90e 100644 --- a/urlpattern.pyi +++ b/urlpattern.pyi @@ -42,7 +42,7 @@ class URLPattern: def __init__(self, input: str, options: None) -> None: ... @overload def __init__( - self, input: URLPatternInit, options: URLPatternOptions = {} + self, input: URLPatternInit = {}, options: URLPatternOptions = {} ) -> None: ... @overload def __init__(self, input: URLPatternInit, options: None) -> None: ... From f41c7779630dfa0e60f00a558628edf7b90866c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 26 Jan 2026 20:22:53 +0000 Subject: [PATCH 41/98] Bump to 0.1.8 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b0a624..95c208f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,7 +280,7 @@ dependencies = [ [[package]] name = "python-urlpattern" -version = "0.1.7" +version = "0.1.8" dependencies = [ "pyo3", "urlpattern", diff --git a/Cargo.toml b/Cargo.toml index 3d3a647..bafa905 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python-urlpattern" -version = "0.1.7" +version = "0.1.8" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" description = "An implementation of the URL Pattern Standard for Python written in Rust." From 26eb996bd746fc17fe0066e3728394d0002f3e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 4 Feb 2026 02:28:45 +0000 Subject: [PATCH 42/98] Update PyO3 to 0.28 --- Cargo.lock | 75 +++++++++++++----------------------------------------- Cargo.toml | 2 +- src/lib.rs | 2 +- 3 files changed, 20 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95c208f..14ee9d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "displaydoc" version = "0.2.5" @@ -145,15 +139,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "libc" version = "0.2.180" @@ -172,15 +157,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -195,9 +171,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -219,35 +195,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" +checksum = "fcf3ccafdf54c050be48a3a086d372f77ba6615f5057211607cd30e5ac5cec6d" dependencies = [ - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" +checksum = "972720a441c91fd9c49f212a1d2d74c6e3803b231ebc8d66c51efbd7ccab11c8" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" +checksum = "5994456d9dab8934d600d3867571b6410f24fbd6002570ad56356733eb54859b" dependencies = [ "libc", "pyo3-build-config", @@ -255,9 +228,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" +checksum = "11ce9cc8d81b3c4969748807604d92b4eef363c5bb82b1a1bdb34ec6f1093a18" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -267,9 +240,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" +checksum = "eaf4b60036a154d23282679b658e3cc7d88d3b8c9a40b43824785f232d2e1b98" dependencies = [ "heck", "proc-macro2", @@ -297,9 +270,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -309,9 +282,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -320,15 +293,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rustversion" -version = "1.0.22" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "serde" @@ -416,12 +383,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "url" version = "2.5.8" diff --git a/Cargo.toml b/Cargo.toml index bafa905..c3a1154 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,5 @@ categories = ["web-programming"] crate-type = ["cdylib"] [dependencies] -pyo3 = "0.27.2" +pyo3 = "0.28.0" deno_urlpattern = { package = "urlpattern", version = "0.5.2" } diff --git a/src/lib.rs b/src/lib.rs index 9e6e278..d50f5bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -353,7 +353,7 @@ impl From for Error { } /// A Python module implemented in Rust. -#[pymodule(gil_used = false)] +#[pymodule] mod urlpattern { #[pymodule_export] use super::UrlPattern; From 95423d4b87d69840765aaffa9e9e4a664d251eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 15 Feb 2026 15:50:16 +0000 Subject: [PATCH 43/98] Update CI.yml to to Maturin 1.12.0 --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 71457b7..426c5d8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,4 @@ -# This file is autogenerated by maturin v1.11.5 +# This file is autogenerated by maturin v1.12.0 # To update, run # # maturin generate-ci --pytest github From aec8a6d3f6dda74a9050a3215dedffc42010347f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 15 Feb 2026 16:16:13 +0000 Subject: [PATCH 44/98] Update denoland/rust-urlpattern to 0.6.0 --- Cargo.lock | 40 ++++++++++++++++++++-------------------- Cargo.toml | 2 +- src/lib.rs | 12 ++++++++---- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14ee9d3..8ee5d04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,9 +141,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "litemap" @@ -153,9 +153,9 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "once_cell" @@ -195,9 +195,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf3ccafdf54c050be48a3a086d372f77ba6615f5057211607cd30e5ac5cec6d" +checksum = "14c738662e2181be11cb82487628404254902bb3225d8e9e99c31f3ef82a405c" dependencies = [ "libc", "once_cell", @@ -209,18 +209,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972720a441c91fd9c49f212a1d2d74c6e3803b231ebc8d66c51efbd7ccab11c8" +checksum = "f9ca0864a7dd3c133a7f3f020cbff2e12e88420da854c35540fd20ce2d60e435" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5994456d9dab8934d600d3867571b6410f24fbd6002570ad56356733eb54859b" +checksum = "9dfc1956b709823164763a34cc42bbfd26b8730afa77809a3df8b94a3ae3b059" dependencies = [ "libc", "pyo3-build-config", @@ -228,9 +228,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ce9cc8d81b3c4969748807604d92b4eef363c5bb82b1a1bdb34ec6f1093a18" +checksum = "29dc660ad948bae134d579661d08033fbb1918f4529c3bbe3257a68f2009ddf2" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf4b60036a154d23282679b658e3cc7d88d3b8c9a40b43824785f232d2e1b98" +checksum = "e78cd6c6d718acfcedf26c3d21fe0f053624368b0d44298c55d7138fde9331f7" dependencies = [ "heck", "proc-macro2", @@ -341,9 +341,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -379,9 +379,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "url" @@ -397,9 +397,9 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ffe0983283c841b6c6e9f116833a0061cc690c0eb67994d65bbdfa5cd3f24e" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" dependencies = [ "icu_properties", "regex", diff --git a/Cargo.toml b/Cargo.toml index c3a1154..c356974 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ crate-type = ["cdylib"] [dependencies] pyo3 = "0.28.0" -deno_urlpattern = { package = "urlpattern", version = "0.5.2" } +deno_urlpattern = { package = "urlpattern", version = "0.6.0" } diff --git a/src/lib.rs b/src/lib.rs index d50f5bd..1dc8f6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ #![allow(non_snake_case)] +use std::borrow::Cow; use std::collections::HashMap; use pyo3::{ @@ -201,13 +202,13 @@ pub enum UrlPatternInput<'py> { Init(Bound<'py, PyDict>), } -impl<'py> TryFrom> for deno_urlpattern::quirks::StringOrInit { +impl<'py> TryFrom> for deno_urlpattern::quirks::StringOrInit<'static> { type Error = pyo3::PyErr; fn try_from(input: UrlPatternInput<'py>) -> Result { Ok(match input { UrlPatternInput::String(pattern) => { - deno_urlpattern::quirks::StringOrInit::String(pattern) + deno_urlpattern::quirks::StringOrInit::String(Cow::Owned(pattern)) } UrlPatternInput::Init(init) => deno_urlpattern::quirks::StringOrInit::Init( deno_urlpattern::quirks::UrlPatternInit { @@ -254,7 +255,10 @@ impl<'py> TryFrom> for deno_urlpattern::quirks::StringOrIni } pub struct UrlPatternResult { - pub inputs: (deno_urlpattern::quirks::StringOrInit, Option), + pub inputs: ( + deno_urlpattern::quirks::StringOrInit<'static>, + Option, + ), pub protocol: UrlPatternComponentResult, pub username: UrlPatternComponentResult, pub password: UrlPatternComponentResult, @@ -278,7 +282,7 @@ impl<'py> IntoPyObject<'py> for UrlPatternResult { match string_or_init { deno_urlpattern::quirks::StringOrInit::String(string) => { - list.append(string).unwrap(); + list.append(string.into_owned()).unwrap(); } deno_urlpattern::quirks::StringOrInit::Init(init) => { let init_dict = PyDict::new(py); From 839ecbd70b302c0da5d99dd00abcb3d3323aa031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 15 Feb 2026 16:17:20 +0000 Subject: [PATCH 45/98] Bump to 0.1.9 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ee5d04..c5e9a4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,7 +253,7 @@ dependencies = [ [[package]] name = "python-urlpattern" -version = "0.1.8" +version = "0.1.9" dependencies = [ "pyo3", "urlpattern", diff --git a/Cargo.toml b/Cargo.toml index c356974..3ad3677 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python-urlpattern" -version = "0.1.8" +version = "0.1.9" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" description = "An implementation of the URL Pattern Standard for Python written in Rust." From 27f8dd653af1fbb65f1d14f494d3f0c16cce60f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 15 Feb 2026 16:49:58 +0000 Subject: [PATCH 46/98] Remove pytest from CI.yml addnab/docker-run-action is using old Docker version, which causes the following error: > docker: Error response from daemon: client version 1.41 is too old. Minimum supported API version is 1.44, please upgrade your client to a newer version. --- .github/workflows/CI.yml | 74 +--------------------------------------- 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 426c5d8..564ae01 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,7 +1,7 @@ # This file is autogenerated by maturin v1.12.0 # To update, run # -# maturin generate-ci --pytest github +# maturin generate-ci github # name: CI @@ -53,31 +53,6 @@ jobs: with: name: wheels-linux-${{ matrix.platform.target }} path: dist - - name: pytest - if: ${{ startsWith(matrix.platform.target, 'x86_64') }} - shell: bash - run: | - set -e - python3 -m venv .venv - source .venv/bin/activate - pip install urlpattern --find-links dist --force-reinstall - pip install pytest - pytest - - name: pytest - if: ${{ !startsWith(matrix.platform.target, 'x86') && matrix.platform.target != 'ppc64' }} - uses: uraimo/run-on-arch-action@v2 - with: - arch: ${{ matrix.platform.target }} - distro: ubuntu22.04 - githubToken: ${{ github.token }} - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip - pip3 install -U pip pytest - run: | - set -e - pip3 install urlpattern --find-links dist --force-reinstall - pytest musllinux: runs-on: ${{ matrix.platform.runner }} @@ -109,36 +84,6 @@ jobs: with: name: wheels-musllinux-${{ matrix.platform.target }} path: dist - - name: pytest - if: ${{ startsWith(matrix.platform.target, 'x86_64') }} - uses: addnab/docker-run-action@v3 - with: - image: alpine:latest - options: -v ${{ github.workspace }}:/io -w /io - run: | - set -e - apk add py3-pip py3-virtualenv - python3 -m virtualenv .venv - source .venv/bin/activate - pip install urlpattern --no-index --find-links dist --force-reinstall - pip install pytest - pytest - - name: pytest - if: ${{ !startsWith(matrix.platform.target, 'x86') }} - uses: uraimo/run-on-arch-action@v2 - with: - arch: ${{ matrix.platform.target }} - distro: alpine_latest - githubToken: ${{ github.token }} - install: | - apk add py3-virtualenv - run: | - set -e - python3 -m virtualenv .venv - source .venv/bin/activate - pip install pytest - pip install urlpattern --find-links dist --force-reinstall - pytest windows: runs-on: ${{ matrix.platform.runner }} @@ -171,15 +116,6 @@ jobs: with: name: wheels-windows-${{ matrix.platform.target }} path: dist - - name: pytest - shell: bash - run: | - set -e - python3 -m venv .venv - source .venv/Scripts/activate - pip install urlpattern --find-links dist --force-reinstall - pip install pytest - pytest macos: runs-on: ${{ matrix.platform.runner }} @@ -206,14 +142,6 @@ jobs: with: name: wheels-macos-${{ matrix.platform.target }} path: dist - - name: pytest - run: | - set -e - python3 -m venv .venv - source .venv/bin/activate - pip install urlpattern --find-links dist --force-reinstall - pip install pytest - pytest sdist: runs-on: ubuntu-latest From 9824a45740e2b736221810474cedbfa061b43180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 18 Feb 2026 11:35:11 +0900 Subject: [PATCH 47/98] Fix baseURL example Updated example usage of URLPattern in README. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6616e5b..4777db5 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,9 @@ print(result["pathname"]["groups"]["id"]) # output: 4163 ```py from urlpattern import URLPattern -pattern = URLPattern({"pathname": "/admin/*"}, "https://example.com") -print(pattern.test({"pathname": "/admin/main/"}, "https://example.com")) # output: True -print(pattern.test("/admin/main/", "https://example.com")) # output: True +pattern = URLPattern("/test", "https://example.com") +print(pattern.test("/test", "https://example.com")) # output: True +print(pattern.test({"pathname": "/test", "baseURL": "https://example.com"})) # output: True ``` ### `ignoreCase` From 67f2f156ae334aebad6589aa40d32da8e63ea4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 18 Feb 2026 11:47:18 +0900 Subject: [PATCH 48/98] Improve baseURL example --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4777db5..d7580b2 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,12 @@ print(result["pathname"]["groups"]["id"]) # output: 4163 ```py from urlpattern import URLPattern -pattern = URLPattern("/test", "https://example.com") -print(pattern.test("/test", "https://example.com")) # output: True -print(pattern.test({"pathname": "/test", "baseURL": "https://example.com"})) # output: True +pattern = URLPattern("b", "https://example.com/a/") +print(pattern.test("a/b", "https://example.com/")) # output: True +print(pattern.test("b", "https://example.com/a/")) # output: True +print( + pattern.test({"pathname": "b", "baseURL": "https://example.com/a/"}) +) # output: True ``` ### `ignoreCase` From c1a0e9e019985d85cea6966e97ab10549fdb031e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:35:28 +0000 Subject: [PATCH 49/98] Bump pyo3 from 0.28.1 to 0.28.2 in the cargo group across 1 directory Bumps the cargo group with 1 update in the / directory: [pyo3](https://github.com/pyo3/pyo3). Updates `pyo3` from 0.28.1 to 0.28.2 - [Release notes](https://github.com/pyo3/pyo3/releases) - [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md) - [Commits](https://github.com/pyo3/pyo3/compare/v0.28.1...v0.28.2) --- updated-dependencies: - dependency-name: pyo3 dependency-version: 0.28.2 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5e9a4b..5a1996d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,9 +195,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c738662e2181be11cb82487628404254902bb3225d8e9e99c31f3ef82a405c" +checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" dependencies = [ "libc", "once_cell", @@ -209,18 +209,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9ca0864a7dd3c133a7f3f020cbff2e12e88420da854c35540fd20ce2d60e435" +checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfc1956b709823164763a34cc42bbfd26b8730afa77809a3df8b94a3ae3b059" +checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" dependencies = [ "libc", "pyo3-build-config", @@ -228,9 +228,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29dc660ad948bae134d579661d08033fbb1918f4529c3bbe3257a68f2009ddf2" +checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78cd6c6d718acfcedf26c3d21fe0f053624368b0d44298c55d7138fde9331f7" +checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" dependencies = [ "heck", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 3ad3677..7478c0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,5 @@ categories = ["web-programming"] crate-type = ["cdylib"] [dependencies] -pyo3 = "0.28.0" +pyo3 = "0.28.2" deno_urlpattern = { package = "urlpattern", version = "0.6.0" } From b80aa443b8358655163574d81a421a53906281d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 21 Feb 2026 05:31:57 +0000 Subject: [PATCH 50/98] Update dependencies --- .github/workflows/CI.yml | 2 +- Cargo.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 564ae01..4b12b15 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,4 @@ -# This file is autogenerated by maturin v1.12.0 +# This file is autogenerated by maturin v1.12.3 # To update, run # # maturin generate-ci github diff --git a/Cargo.lock b/Cargo.lock index 5a1996d..5e526df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,9 +341,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.115" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -363,9 +363,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tinystr" @@ -379,9 +379,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "url" From b3410fe2c94e8ebea0a5b8d8d82ed66017625aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 21 Feb 2026 05:41:41 +0000 Subject: [PATCH 51/98] Bump to 0.1.10 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e526df..e96328a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,7 +253,7 @@ dependencies = [ [[package]] name = "python-urlpattern" -version = "0.1.9" +version = "0.1.10" dependencies = [ "pyo3", "urlpattern", diff --git a/Cargo.toml b/Cargo.toml index 7478c0d..4d13888 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python-urlpattern" -version = "0.1.9" +version = "0.1.10" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" description = "An implementation of the URL Pattern Standard for Python written in Rust." From 9207147031ec548b7db3555a6d30860ba9e316e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Tue, 24 Mar 2026 01:33:57 +0900 Subject: [PATCH 52/98] Change header from 'URL Pattern' to 'urlpattern' --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7580b2..3141e14 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# URL Pattern +# urlpattern [![PyPI - Version](https://img.shields.io/pypi/v/urlpattern)](https://pypi.org/project/urlpattern/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/urlpattern)](https://pypi.org/project/urlpattern/) From 8eaf7dbf8cff2173ac171d952f67266069463798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Fri, 27 Mar 2026 16:54:35 +0000 Subject: [PATCH 53/98] Update urlpatterntestdata.json --- tests/test_lib.py | 2 +- tests/urlpatterntestdata.json | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/test_lib.py b/tests/test_lib.py index b44c67d..fe0884f 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -19,7 +19,7 @@ @pytest.mark.parametrize("entry", urlpatterntestdata) def test(entry): - if entry["pattern"] == [{"pathname": "*{}**?"}]: + if entry["pattern"] == [{"pathname": "*{}**?"}] or entry["pattern"] == ["((?R)):"]: pytest.skip("unsupported in the implementation") if entry.get("expected_obj") == "error": diff --git a/tests/urlpatterntestdata.json b/tests/urlpatterntestdata.json index ebc171c..6eb4d3f 100644 --- a/tests/urlpatterntestdata.json +++ b/tests/urlpatterntestdata.json @@ -3118,5 +3118,41 @@ "expected_match": { "hostname": { "input": "localhost", "groups": { "domain" : "localhost"} } } + }, + { + "pattern": ["((?R)):"], + "expected_obj": "error" + }, + { + "pattern": ["(\\H):"], + "expected_obj": "error" + }, + { + "pattern": [ + {"pathname": "/:foo((?a))"} + ], + "inputs": [ + {"pathname": "/a"} + ], + "expected_match": { + "pathname": { + "input": "/a", + "groups": {"foo": "a"} + } + } + }, + { + "pattern": [ + {"pathname": "/foo/(bar(?baz))"} + ], + "inputs": [ + {"pathname": "/foo/barbaz"} + ], + "expected_match": { + "pathname": { + "input": "/foo/barbaz", + "groups": {"0": "barbaz"} + } + } } ] \ No newline at end of file From 620d9f4b15a4dae04e8f05581af86c535c79d78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Fri, 27 Mar 2026 17:23:04 +0000 Subject: [PATCH 54/98] Rename package from `python-urlpattern` to `urlpattern` --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e96328a..e860459 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,14 +251,6 @@ dependencies = [ "syn", ] -[[package]] -name = "python-urlpattern" -version = "0.1.10" -dependencies = [ - "pyo3", - "urlpattern", -] - [[package]] name = "quote" version = "1.0.44" @@ -395,6 +387,14 @@ dependencies = [ "serde", ] +[[package]] +name = "urlpattern" +version = "0.1.10" +dependencies = [ + "pyo3", + "urlpattern 0.6.0", +] + [[package]] name = "urlpattern" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 4d13888..0d7548d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "python-urlpattern" +name = "urlpattern" version = "0.1.10" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" From 601124c22b9b7ec16b457e131879d39fc20d8fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Fri, 27 Mar 2026 18:07:03 +0000 Subject: [PATCH 55/98] Rename to deno-urlpattern --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0d7548d..3382a2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ crate-type = ["cdylib"] [dependencies] pyo3 = "0.28.2" -deno_urlpattern = { package = "urlpattern", version = "0.6.0" } +deno-urlpattern = { package = "urlpattern", version = "0.6.0" } From 4da1bb504ce8b59e622e08519e7e6a27adf28593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Fri, 27 Mar 2026 18:09:12 +0000 Subject: [PATCH 56/98] Fix package name declaration order --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3382a2c..d00c872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ crate-type = ["cdylib"] [dependencies] pyo3 = "0.28.2" -deno-urlpattern = { package = "urlpattern", version = "0.6.0" } +deno-urlpattern = { version = "0.6.0", package = "urlpattern" } From a038320c837c356c33335b92fe2d977f6e83d335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 29 Mar 2026 08:58:10 +0000 Subject: [PATCH 57/98] Configure Ruff to format Python code on-save --- .vscode/settings.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 201a6d5..1ea81b1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,12 @@ { - "python.defaultInterpreterPath": "${workspaceFolder}/.venv", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv", "python.testing.pytestArgs": [ "tests" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff" + } } \ No newline at end of file From 82b9ca91e5b2b43c37a37d7333fa659f77da5e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 29 Mar 2026 14:55:56 +0000 Subject: [PATCH 58/98] Add hasRegExpGroups --- src/lib.rs | 6 ++++++ urlpattern.pyi | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 1dc8f6a..26ce6e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,6 +76,7 @@ impl UrlPattern { dict.set_item("pathname", self.0.pathname()).unwrap(); dict.set_item("search", self.0.search()).unwrap(); dict.set_item("hash", self.0.hash()).unwrap(); + dict.set_item("hasRegExpGroups", self.0.has_regexp_groups()).unwrap(); format!("URLPattern({})", dict) } @@ -194,6 +195,11 @@ impl UrlPattern { pub fn get_hash(&self) -> PyResult<&str> { Ok(self.0.hash()) } + + #[getter] + pub fn get_hasRegExpGroups(&self) -> PyResult { + Ok(self.0.has_regexp_groups()) + } } #[derive(FromPyObject)] diff --git a/urlpattern.pyi b/urlpattern.pyi index 73fb90e..80ce214 100644 --- a/urlpattern.pyi +++ b/urlpattern.pyi @@ -76,6 +76,8 @@ class URLPattern: def search(self) -> str: ... @property def hash(self) -> str: ... + @property + def hasRegExpGroups(self) -> bool: ... class URLPatternInit(TypedDict, total=False): protocol: str From ed067bd248e1831ee8f5815a5949c19f5aef6906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 29 Mar 2026 15:06:16 +0000 Subject: [PATCH 59/98] Update dependencies --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e860459..1578fbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,9 +141,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "litemap" @@ -159,9 +159,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "percent-encoding" @@ -253,9 +253,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -285,9 +285,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "serde" From 2445fd6b51bc64d2d61b54a16a1263ce3fe8151d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 29 Mar 2026 15:07:03 +0000 Subject: [PATCH 60/98] Bump to 0.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1578fbc..04b5e7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,7 +389,7 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.1.10" +version = "0.2.0" dependencies = [ "pyo3", "urlpattern 0.6.0", diff --git a/Cargo.toml b/Cargo.toml index d00c872..3d1c2d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "urlpattern" -version = "0.1.10" +version = "0.2.0" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" description = "An implementation of the URL Pattern Standard for Python written in Rust." From 4ac176a9dc43d915441b201db371aab4bd467286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 29 Mar 2026 15:18:08 +0000 Subject: [PATCH 61/98] Remove `get_` prefix --- src/lib.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 26ce6e5..4d7a881 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,47 +157,47 @@ impl UrlPattern { } #[getter] - pub fn get_protocol(&self) -> PyResult<&str> { + pub fn protocol(&self) -> PyResult<&str> { Ok(self.0.protocol()) } #[getter] - pub fn get_username(&self) -> PyResult<&str> { + pub fn username(&self) -> PyResult<&str> { Ok(self.0.username()) } #[getter] - pub fn get_password(&self) -> PyResult<&str> { + pub fn password(&self) -> PyResult<&str> { Ok(self.0.password()) } #[getter] - pub fn get_hostname(&self) -> PyResult<&str> { + pub fn hostname(&self) -> PyResult<&str> { Ok(self.0.hostname()) } #[getter] - pub fn get_port(&self) -> PyResult<&str> { + pub fn port(&self) -> PyResult<&str> { Ok(self.0.port()) } #[getter] - pub fn get_pathname(&self) -> PyResult<&str> { + pub fn pathname(&self) -> PyResult<&str> { Ok(self.0.pathname()) } #[getter] - pub fn get_search(&self) -> PyResult<&str> { + pub fn search(&self) -> PyResult<&str> { Ok(self.0.search()) } #[getter] - pub fn get_hash(&self) -> PyResult<&str> { + pub fn hash(&self) -> PyResult<&str> { Ok(self.0.hash()) } #[getter] - pub fn get_hasRegExpGroups(&self) -> PyResult { + pub fn hasRegExpGroups(&self) -> PyResult { Ok(self.0.has_regexp_groups()) } } From 1eac2c5fbcca51a99a49fb968501807db7fd202e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 29 Mar 2026 15:21:25 +0000 Subject: [PATCH 62/98] Rename to `has_regexp_groups` --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4d7a881..d669d98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,8 +196,8 @@ impl UrlPattern { Ok(self.0.hash()) } - #[getter] - pub fn hasRegExpGroups(&self) -> PyResult { + #[getter(hasRegExpGroups)] + pub fn has_regexp_groups(&self) -> PyResult { Ok(self.0.has_regexp_groups()) } } From bfd3fcbf9988d0a4121fdad96012e454c343bfac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 29 Mar 2026 15:51:20 +0000 Subject: [PATCH 63/98] Add explanation for camelCase --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 3141e14..0c4a0e3 100644 --- a/README.md +++ b/README.md @@ -125,3 +125,9 @@ with make_server("", 8000, app) as httpd: Due to limitations in the dependency [denoland/rust-urlpattern](https://github.com/denoland/rust-urlpattern), it may not support all features specified in [the standard](https://urlpattern.spec.whatwg.org/). Check `pytest.skip` in [`tests/test_lib.py`](https://github.com/urlpattern/python-urlpattern/blob/main/tests/test_lib.py). + +## Why camelCase? + +In this library, some names such as `baseURL` and `hasRegExpGroups` do not use snake_case. + +Like [xml.dom](https://docs.python.org/3/library/xml.dom.html), Python wrappers around web standards typically preserve the original camelCase rather than converting names to snake_case. This library follows that convention as well. From 9f703c20fe2c6399188bc2f160184bf2904a06d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 30 Mar 2026 22:04:26 +0900 Subject: [PATCH 64/98] Fix code reference formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c4a0e3..02af0a7 100644 --- a/README.md +++ b/README.md @@ -130,4 +130,4 @@ Check `pytest.skip` in [`tests/test_lib.py`](https://github.com/urlpattern/pytho In this library, some names such as `baseURL` and `hasRegExpGroups` do not use snake_case. -Like [xml.dom](https://docs.python.org/3/library/xml.dom.html), Python wrappers around web standards typically preserve the original camelCase rather than converting names to snake_case. This library follows that convention as well. +Like [`xml.dom`](https://docs.python.org/3/library/xml.dom.html), Python wrappers around web standards typically preserve the original camelCase rather than converting names to snake_case. This library follows that convention as well. From aa93e7455570bed5347d9c9b627026f8deca74af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 30 Mar 2026 14:51:01 +0000 Subject: [PATCH 65/98] Remove pub --- src/lib.rs | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d669d98..c853701 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ struct UrlPattern(deno_urlpattern::UrlPattern); impl UrlPattern { #[new] #[pyo3(signature = (input=None, baseURL=None, options=None))] - pub fn new( + fn new( input: Option, baseURL: Option<&Bound<'_, PyAny>>, options: Option<&Bound<'_, PyDict>>, @@ -66,7 +66,7 @@ impl UrlPattern { )) } - pub fn __repr__(&self, py: Python) -> String { + fn __repr__(&self, py: Python) -> String { let dict = PyDict::new(py); dict.set_item("protocol", self.0.protocol()).unwrap(); dict.set_item("username", self.0.username()).unwrap(); @@ -81,7 +81,7 @@ impl UrlPattern { } #[pyo3(signature = (input=None, baseURL=None))] - pub fn test(&self, input: Option, baseURL: Option<&str>) -> PyResult { + fn test(&self, input: Option, baseURL: Option<&str>) -> PyResult { let string_or_init_input = match input { Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, None => deno_urlpattern::quirks::StringOrInit::Init( @@ -98,7 +98,7 @@ impl UrlPattern { } #[pyo3(signature = (input=None, baseURL=None))] - pub fn exec( + fn exec( &self, input: Option, baseURL: Option<&str>, @@ -157,53 +157,53 @@ impl UrlPattern { } #[getter] - pub fn protocol(&self) -> PyResult<&str> { + fn protocol(&self) -> PyResult<&str> { Ok(self.0.protocol()) } #[getter] - pub fn username(&self) -> PyResult<&str> { + fn username(&self) -> PyResult<&str> { Ok(self.0.username()) } #[getter] - pub fn password(&self) -> PyResult<&str> { + fn password(&self) -> PyResult<&str> { Ok(self.0.password()) } #[getter] - pub fn hostname(&self) -> PyResult<&str> { + fn hostname(&self) -> PyResult<&str> { Ok(self.0.hostname()) } #[getter] - pub fn port(&self) -> PyResult<&str> { + fn port(&self) -> PyResult<&str> { Ok(self.0.port()) } #[getter] - pub fn pathname(&self) -> PyResult<&str> { + fn pathname(&self) -> PyResult<&str> { Ok(self.0.pathname()) } #[getter] - pub fn search(&self) -> PyResult<&str> { + fn search(&self) -> PyResult<&str> { Ok(self.0.search()) } #[getter] - pub fn hash(&self) -> PyResult<&str> { + fn hash(&self) -> PyResult<&str> { Ok(self.0.hash()) } #[getter(hasRegExpGroups)] - pub fn has_regexp_groups(&self) -> PyResult { + fn has_regexp_groups(&self) -> PyResult { Ok(self.0.has_regexp_groups()) } } #[derive(FromPyObject)] -pub enum UrlPatternInput<'py> { +enum UrlPatternInput<'py> { String(String), Init(Bound<'py, PyDict>), } @@ -260,19 +260,19 @@ impl<'py> TryFrom> for deno_urlpattern::quirks::StringOrIni } } -pub struct UrlPatternResult { - pub inputs: ( +struct UrlPatternResult { + inputs: ( deno_urlpattern::quirks::StringOrInit<'static>, Option, ), - pub protocol: UrlPatternComponentResult, - pub username: UrlPatternComponentResult, - pub password: UrlPatternComponentResult, - pub hostname: UrlPatternComponentResult, - pub port: UrlPatternComponentResult, - pub pathname: UrlPatternComponentResult, - pub search: UrlPatternComponentResult, - pub hash: UrlPatternComponentResult, + protocol: UrlPatternComponentResult, + username: UrlPatternComponentResult, + password: UrlPatternComponentResult, + hostname: UrlPatternComponentResult, + port: UrlPatternComponentResult, + pathname: UrlPatternComponentResult, + search: UrlPatternComponentResult, + hash: UrlPatternComponentResult, } impl<'py> IntoPyObject<'py> for UrlPatternResult { @@ -343,12 +343,12 @@ impl<'py> IntoPyObject<'py> for UrlPatternResult { } #[derive(IntoPyObject, IntoPyObjectRef)] -pub struct UrlPatternComponentResult { +struct UrlPatternComponentResult { input: String, groups: HashMap>, } -pub struct Error(deno_urlpattern::Error); +struct Error(deno_urlpattern::Error); impl From for PyErr { fn from(error: Error) -> Self { From 699ac511cd42bfae17b72926bcb27f61f40c9830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 30 Mar 2026 14:51:40 +0000 Subject: [PATCH 66/98] Format --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c853701..e8737e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,7 +76,8 @@ impl UrlPattern { dict.set_item("pathname", self.0.pathname()).unwrap(); dict.set_item("search", self.0.search()).unwrap(); dict.set_item("hash", self.0.hash()).unwrap(); - dict.set_item("hasRegExpGroups", self.0.has_regexp_groups()).unwrap(); + dict.set_item("hasRegExpGroups", self.0.has_regexp_groups()) + .unwrap(); format!("URLPattern({})", dict) } From b94998c38406b93ec3a5e2529202debc790303c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 30 Mar 2026 16:06:47 +0000 Subject: [PATCH 67/98] Refactor `UrlPattern::new` --- src/lib.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e8737e8..c07c38a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,12 @@ impl UrlPattern { baseURL: Option<&Bound<'_, PyAny>>, options: Option<&Bound<'_, PyDict>>, ) -> PyResult { + let string_or_init_input = match input { + Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, + None => deno_urlpattern::quirks::StringOrInit::Init( + deno_urlpattern::quirks::UrlPatternInit::default(), + ), + }; let (base_url, options) = match baseURL { Some(value) => { if let Ok(options_dict) = value.cast::() { @@ -34,13 +40,6 @@ impl UrlPattern { } None => (None, options), }; - - let string_or_init_input = match input { - Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, - None => deno_urlpattern::quirks::StringOrInit::Init( - deno_urlpattern::quirks::UrlPatternInit::default(), - ), - }; let options = if let Some(options) = options { deno_urlpattern::UrlPatternOptions { ignore_case: options From fc9fbc891e1dba9e7831850547b63a92aa7a2b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Tue, 31 Mar 2026 18:35:28 +0000 Subject: [PATCH 68/98] Stop using quirks module --- Cargo.lock | 2 + Cargo.toml | 4 +- src/lib.rs | 405 +++++++++++++++++++++++++++++++++-------------------- 3 files changed, 262 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04b5e7e..cdc10fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,6 +392,8 @@ name = "urlpattern" version = "0.2.0" dependencies = [ "pyo3", + "regex", + "url", "urlpattern 0.6.0", ] diff --git a/Cargo.toml b/Cargo.toml index 3d1c2d5..70733c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,7 @@ categories = ["web-programming"] crate-type = ["cdylib"] [dependencies] -pyo3 = "0.28.2" deno-urlpattern = { version = "0.6.0", package = "urlpattern" } +pyo3 = "0.28.2" +regex = "1.12.3" +url = "2.5.8" diff --git a/src/lib.rs b/src/lib.rs index c07c38a..f16efdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,12 @@ #![allow(non_snake_case)] -use std::borrow::Cow; -use std::collections::HashMap; - use pyo3::{ BoundObject, - exceptions::PyValueError, + exceptions::{PyTypeError, PyValueError}, prelude::*, - types::{PyDict, PyList}, + types::{PyDict, PyList, PyString}, }; +use std::collections::HashMap; #[pyclass(name = "URLPattern")] struct UrlPattern(deno_urlpattern::UrlPattern); @@ -22,12 +20,6 @@ impl UrlPattern { baseURL: Option<&Bound<'_, PyAny>>, options: Option<&Bound<'_, PyDict>>, ) -> PyResult { - let string_or_init_input = match input { - Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, - None => deno_urlpattern::quirks::StringOrInit::Init( - deno_urlpattern::quirks::UrlPatternInit::default(), - ), - }; let (base_url, options) = match baseURL { Some(value) => { if let Ok(options_dict) = value.cast::() { @@ -35,11 +27,81 @@ impl UrlPattern { } else if value.is_none() { (None, options) } else { - (Some(value.extract::()?), options) + ( + Some( + value + .extract::()? + .parse::() + .map_err(deno_urlpattern::Error::Url) + .map_err(Error)?, + ), + options, + ) } } None => (None, options), }; + + if let Some(UrlPatternInput::Init(_)) = input { + if let Some(_) = base_url { + return Err(PyTypeError::new_err("cannot use dict input with baseURL")); + } + } + + let init: deno_urlpattern::UrlPatternInit = match input { + Some(input) => match input { + UrlPatternInput::String(input) => { + deno_urlpattern::UrlPatternInit::parse_constructor_string::( + input.as_str(), + base_url, + ) + .map_err(Error)? + } + UrlPatternInput::Init(init) => deno_urlpattern::UrlPatternInit { + protocol: init + .get_item("protocol")? + .map(|v| v.extract::()) + .transpose()?, + username: init + .get_item("username")? + .map(|v| v.extract::()) + .transpose()?, + password: init + .get_item("password")? + .map(|v| v.extract::()) + .transpose()?, + hostname: init + .get_item("hostname")? + .map(|v| v.extract::()) + .transpose()?, + port: init + .get_item("port")? + .map(|v| v.extract::()) + .transpose()?, + pathname: init + .get_item("pathname")? + .map(|v| v.extract::()) + .transpose()?, + search: init + .get_item("search")? + .map(|v| v.extract::()) + .transpose()?, + hash: init + .get_item("hash")? + .map(|v| v.extract::()) + .transpose()?, + base_url: init + .get_item("baseURL")? + .map(|v| v.extract::()) + .transpose()? + .map(|v| v.parse::()) + .transpose() + .map_err(deno_urlpattern::Error::Url) + .map_err(Error)?, + }, + }, + None => deno_urlpattern::UrlPatternInit::default(), + }; let options = if let Some(options) = options { deno_urlpattern::UrlPatternOptions { ignore_case: options @@ -53,15 +115,7 @@ impl UrlPattern { deno_urlpattern::UrlPatternOptions::default() }; Ok(UrlPattern( - ::parse( - deno_urlpattern::quirks::process_construct_pattern_input( - string_or_init_input, - base_url.as_deref(), - ) - .map_err(Error)?, - options, - ) - .map_err(Error)?, + deno_urlpattern::UrlPattern::parse(init, options).map_err(Error)?, )) } @@ -82,45 +136,189 @@ impl UrlPattern { #[pyo3(signature = (input=None, baseURL=None))] fn test(&self, input: Option, baseURL: Option<&str>) -> PyResult { - let string_or_init_input = match input { - Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, - None => deno_urlpattern::quirks::StringOrInit::Init( - deno_urlpattern::quirks::UrlPatternInit::default(), + let input: deno_urlpattern::UrlPatternMatchInput = match input { + Some(input) => match input { + UrlPatternInput::String(input) => match baseURL { + Some(base_url) => { + let base_url = match url::Url::parse(base_url) { + Ok(url) => url, + Err(_) => return Ok(false), + }; + deno_urlpattern::UrlPatternMatchInput::Url( + match url::Url::options() + .base_url(Some(&base_url)) + .parse(input.as_ref()) + { + Ok(url) => url, + Err(_) => return Ok(false), + }, + ) + } + None => deno_urlpattern::UrlPatternMatchInput::Url( + match input.parse::() { + Ok(url) => url, + Err(_) => return Ok(false), + }, + ), + }, + UrlPatternInput::Init(init) => { + if let Some(_) = baseURL { + return Err(PyTypeError::new_err("cannot use dict input with baseURL")); + } + + deno_urlpattern::UrlPatternMatchInput::Init(deno_urlpattern::UrlPatternInit { + protocol: init + .get_item("protocol")? + .map(|v| v.extract::()) + .transpose()?, + username: init + .get_item("username")? + .map(|v| v.extract::()) + .transpose()?, + password: init + .get_item("password")? + .map(|v| v.extract::()) + .transpose()?, + hostname: init + .get_item("hostname")? + .map(|v| v.extract::()) + .transpose()?, + port: init + .get_item("port")? + .map(|v| v.extract::()) + .transpose()?, + pathname: init + .get_item("pathname")? + .map(|v| v.extract::()) + .transpose()?, + search: init + .get_item("search")? + .map(|v| v.extract::()) + .transpose()?, + hash: init + .get_item("hash")? + .map(|v| v.extract::()) + .transpose()?, + base_url: init + .get_item("baseURL")? + .map(|v| v.extract::()) + .transpose()? + .map(|v| v.parse::()) + .transpose() + .map_err(deno_urlpattern::Error::Url) + .map_err(Error)?, + }) + } + }, + None => deno_urlpattern::UrlPatternMatchInput::Init( + deno_urlpattern::UrlPatternInit::default(), ), }; - let Some((match_input, _)) = - deno_urlpattern::quirks::process_match_input(string_or_init_input, baseURL) - .map_err(Error)? - else { - return Ok(false); - }; - Ok(self.0.test(match_input).map_err(Error)?) + Ok(self.0.test(input).map_err(Error)?) } #[pyo3(signature = (input=None, baseURL=None))] - fn exec( + fn exec<'py>( &self, - input: Option, - baseURL: Option<&str>, - ) -> PyResult> { - let string_or_init_input = match input { - Some(input) => deno_urlpattern::quirks::StringOrInit::try_from(input)?, - None => deno_urlpattern::quirks::StringOrInit::Init( - deno_urlpattern::quirks::UrlPatternInit::default(), + py: Python<'py>, + input: Option<&Bound<'py, PyAny>>, + baseURL: Option<&Bound<'py, PyString>>, + ) -> PyResult>> { + let urlpattern_input: Option = input.map(|i| i.extract()).transpose()?; + let input: deno_urlpattern::UrlPatternMatchInput = match &urlpattern_input { + Some(input) => match input { + UrlPatternInput::String(input) => match baseURL { + Some(base_url) => { + let base_url = match url::Url::parse(base_url.to_str()?) { + Ok(url) => url, + Err(_) => return Ok(None), + }; + deno_urlpattern::UrlPatternMatchInput::Url( + match url::Url::options() + .base_url(Some(&base_url)) + .parse(input.as_ref()) + { + Ok(url) => url, + Err(_) => return Ok(None), + }, + ) + } + None => deno_urlpattern::UrlPatternMatchInput::Url( + match input.parse::() { + Ok(url) => url, + Err(_) => return Ok(None), + }, + ), + }, + UrlPatternInput::Init(init) => { + if let Some(_) = baseURL { + return Err(PyTypeError::new_err("cannot use dict input with baseURL")); + } + + deno_urlpattern::UrlPatternMatchInput::Init(deno_urlpattern::UrlPatternInit { + protocol: init + .get_item("protocol")? + .map(|v| v.extract::()) + .transpose()?, + username: init + .get_item("username")? + .map(|v| v.extract::()) + .transpose()?, + password: init + .get_item("password")? + .map(|v| v.extract::()) + .transpose()?, + hostname: init + .get_item("hostname")? + .map(|v| v.extract::()) + .transpose()?, + port: init + .get_item("port")? + .map(|v| v.extract::()) + .transpose()?, + pathname: init + .get_item("pathname")? + .map(|v| v.extract::()) + .transpose()?, + search: init + .get_item("search")? + .map(|v| v.extract::()) + .transpose()?, + hash: init + .get_item("hash")? + .map(|v| v.extract::()) + .transpose()?, + base_url: init + .get_item("baseURL")? + .map(|v| v.extract::()) + .transpose()? + .map(|v| v.parse::()) + .transpose() + .map_err(deno_urlpattern::Error::Url) + .map_err(Error)?, + }) + } + }, + None => deno_urlpattern::UrlPatternMatchInput::Init( + deno_urlpattern::UrlPatternInit::default(), ), }; - let Some((match_input, inputs)) = - deno_urlpattern::quirks::process_match_input(string_or_init_input, baseURL) - .map_err(Error)? - else { - return Ok(None); - }; - let Some(result) = self.0.exec(match_input).map_err(Error)? else { + + let Some(result) = self.0.exec(input).map_err(Error)? else { return Ok(None); }; Ok(Some(UrlPatternResult { - inputs, + inputs: { + let mut vec = Vec::new(); + vec.push( + urlpattern_input.unwrap_or(UrlPatternInput::Init(PyDict::new(py).into_bound())), + ); + if let Some(base_url) = baseURL { + vec.push(UrlPatternInput::String(base_url.to_string())); + } + vec + }, protocol: UrlPatternComponentResult { input: result.protocol.input, groups: result.protocol.groups, @@ -208,63 +406,8 @@ enum UrlPatternInput<'py> { Init(Bound<'py, PyDict>), } -impl<'py> TryFrom> for deno_urlpattern::quirks::StringOrInit<'static> { - type Error = pyo3::PyErr; - - fn try_from(input: UrlPatternInput<'py>) -> Result { - Ok(match input { - UrlPatternInput::String(pattern) => { - deno_urlpattern::quirks::StringOrInit::String(Cow::Owned(pattern)) - } - UrlPatternInput::Init(init) => deno_urlpattern::quirks::StringOrInit::Init( - deno_urlpattern::quirks::UrlPatternInit { - protocol: init - .get_item("protocol")? - .map(|v| v.extract::()) - .transpose()?, - username: init - .get_item("username")? - .map(|v| v.extract::()) - .transpose()?, - password: init - .get_item("password")? - .map(|v| v.extract::()) - .transpose()?, - hostname: init - .get_item("hostname")? - .map(|v| v.extract::()) - .transpose()?, - port: init - .get_item("port")? - .map(|v| v.extract::()) - .transpose()?, - pathname: init - .get_item("pathname")? - .map(|v| v.extract::()) - .transpose()?, - search: init - .get_item("search")? - .map(|v| v.extract::()) - .transpose()?, - hash: init - .get_item("hash")? - .map(|v| v.extract::()) - .transpose()?, - base_url: init - .get_item("baseURL")? - .map(|v| v.extract::()) - .transpose()?, - }, - ), - }) - } -} - -struct UrlPatternResult { - inputs: ( - deno_urlpattern::quirks::StringOrInit<'static>, - Option, - ), +struct UrlPatternResult<'py> { + inputs: Vec>, protocol: UrlPatternComponentResult, username: UrlPatternComponentResult, password: UrlPatternComponentResult, @@ -275,7 +418,7 @@ struct UrlPatternResult { hash: UrlPatternComponentResult, } -impl<'py> IntoPyObject<'py> for UrlPatternResult { +impl<'py> IntoPyObject<'py> for UrlPatternResult<'py> { type Target = PyDict; type Output = Bound<'py, Self::Target>; type Error = std::convert::Infallible; @@ -283,52 +426,19 @@ impl<'py> IntoPyObject<'py> for UrlPatternResult { fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); - let (string_or_init, base_url) = self.inputs; - let list = PyList::empty(py); - - match string_or_init { - deno_urlpattern::quirks::StringOrInit::String(string) => { - list.append(string.into_owned()).unwrap(); - } - deno_urlpattern::quirks::StringOrInit::Init(init) => { - let init_dict = PyDict::new(py); - if let Some(protocol) = init.protocol { - init_dict.set_item("protocol", protocol).unwrap(); - } - if let Some(username) = init.username { - init_dict.set_item("username", username).unwrap(); - } - if let Some(password) = init.password { - init_dict.set_item("password", password).unwrap(); - } - if let Some(hostname) = init.hostname { - init_dict.set_item("hostname", hostname).unwrap(); - } - if let Some(port) = init.port { - init_dict.set_item("port", port).unwrap(); + let inputs = PyList::empty(py); + for input in self.inputs { + match input { + UrlPatternInput::String(string) => { + inputs.append(string).unwrap(); } - if let Some(pathname) = init.pathname { - init_dict.set_item("pathname", pathname).unwrap(); + UrlPatternInput::Init(init) => { + inputs.append(init).unwrap(); } - if let Some(search) = init.search { - init_dict.set_item("search", search).unwrap(); - } - if let Some(hash) = init.hash { - init_dict.set_item("hash", hash).unwrap(); - } - if let Some(base_url) = init.base_url { - init_dict.set_item("baseURL", base_url).unwrap(); - } - list.append(init_dict).unwrap(); } } - if let Some(base_url) = base_url { - list.append(base_url).unwrap(); - } - - dict.set_item("inputs", list).unwrap(); - + dict.set_item("inputs", inputs).unwrap(); dict.set_item("protocol", self.protocol).unwrap(); dict.set_item("username", self.username).unwrap(); dict.set_item("password", self.password).unwrap(); @@ -337,7 +447,6 @@ impl<'py> IntoPyObject<'py> for UrlPatternResult { dict.set_item("pathname", self.pathname).unwrap(); dict.set_item("search", self.search).unwrap(); dict.set_item("hash", self.hash).unwrap(); - Ok(dict.into_bound()) } } From f624ba432acd7a7a2b64b19bbd323630c1e946d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Tue, 31 Mar 2026 18:42:17 +0000 Subject: [PATCH 69/98] Bump to 0.3.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cdc10fe..5db00a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,7 +389,7 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.2.0" +version = "0.3.0" dependencies = [ "pyo3", "regex", diff --git a/Cargo.toml b/Cargo.toml index 70733c0..3fd608f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "urlpattern" -version = "0.2.0" +version = "0.3.0" authors = ["방성범 (Bang Seongbeom) "] edition = "2024" description = "An implementation of the URL Pattern Standard for Python written in Rust." From cd04565b019713667b9dff048436e69f24f804f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 13:59:46 +0000 Subject: [PATCH 70/98] Use `Self` instead of `UrlPattern` --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f16efdd..a044be2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,7 +114,7 @@ impl UrlPattern { } else { deno_urlpattern::UrlPatternOptions::default() }; - Ok(UrlPattern( + Ok(Self( deno_urlpattern::UrlPattern::parse(init, options).map_err(Error)?, )) } From 96087adafbfad335e4198231509e4e3d65f528ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 14:15:00 +0000 Subject: [PATCH 71/98] Replace `deno_urlpattern` with `::urlpattern` --- Cargo.toml | 2 +- src/lib.rs | 56 +++++++++++++++++++++++++++--------------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3fd608f..4df897c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ categories = ["web-programming"] crate-type = ["cdylib"] [dependencies] -deno-urlpattern = { version = "0.6.0", package = "urlpattern" } pyo3 = "0.28.2" regex = "1.12.3" url = "2.5.8" +urlpattern = "0.6.0" diff --git a/src/lib.rs b/src/lib.rs index a044be2..844a072 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use pyo3::{ use std::collections::HashMap; #[pyclass(name = "URLPattern")] -struct UrlPattern(deno_urlpattern::UrlPattern); +struct UrlPattern(::urlpattern::UrlPattern); #[pymethods] impl UrlPattern { @@ -32,7 +32,7 @@ impl UrlPattern { value .extract::()? .parse::() - .map_err(deno_urlpattern::Error::Url) + .map_err(::urlpattern::Error::Url) .map_err(Error)?, ), options, @@ -48,16 +48,16 @@ impl UrlPattern { } } - let init: deno_urlpattern::UrlPatternInit = match input { + let init: ::urlpattern::UrlPatternInit = match input { Some(input) => match input { UrlPatternInput::String(input) => { - deno_urlpattern::UrlPatternInit::parse_constructor_string::( + ::urlpattern::UrlPatternInit::parse_constructor_string::( input.as_str(), base_url, ) .map_err(Error)? } - UrlPatternInput::Init(init) => deno_urlpattern::UrlPatternInit { + UrlPatternInput::Init(init) => ::urlpattern::UrlPatternInit { protocol: init .get_item("protocol")? .map(|v| v.extract::()) @@ -96,26 +96,26 @@ impl UrlPattern { .transpose()? .map(|v| v.parse::()) .transpose() - .map_err(deno_urlpattern::Error::Url) + .map_err(::urlpattern::Error::Url) .map_err(Error)?, }, }, - None => deno_urlpattern::UrlPatternInit::default(), + None => ::urlpattern::UrlPatternInit::default(), }; let options = if let Some(options) = options { - deno_urlpattern::UrlPatternOptions { + ::urlpattern::UrlPatternOptions { ignore_case: options .get_item("ignoreCase")? .map(|v| v.extract::()) .transpose()? .unwrap_or(false), - ..deno_urlpattern::UrlPatternOptions::default() + ..::urlpattern::UrlPatternOptions::default() } } else { - deno_urlpattern::UrlPatternOptions::default() + ::urlpattern::UrlPatternOptions::default() }; Ok(Self( - deno_urlpattern::UrlPattern::parse(init, options).map_err(Error)?, + ::urlpattern::UrlPattern::parse(init, options).map_err(Error)?, )) } @@ -136,7 +136,7 @@ impl UrlPattern { #[pyo3(signature = (input=None, baseURL=None))] fn test(&self, input: Option, baseURL: Option<&str>) -> PyResult { - let input: deno_urlpattern::UrlPatternMatchInput = match input { + let input: ::urlpattern::UrlPatternMatchInput = match input { Some(input) => match input { UrlPatternInput::String(input) => match baseURL { Some(base_url) => { @@ -144,7 +144,7 @@ impl UrlPattern { Ok(url) => url, Err(_) => return Ok(false), }; - deno_urlpattern::UrlPatternMatchInput::Url( + ::urlpattern::UrlPatternMatchInput::Url( match url::Url::options() .base_url(Some(&base_url)) .parse(input.as_ref()) @@ -154,7 +154,7 @@ impl UrlPattern { }, ) } - None => deno_urlpattern::UrlPatternMatchInput::Url( + None => ::urlpattern::UrlPatternMatchInput::Url( match input.parse::() { Ok(url) => url, Err(_) => return Ok(false), @@ -166,7 +166,7 @@ impl UrlPattern { return Err(PyTypeError::new_err("cannot use dict input with baseURL")); } - deno_urlpattern::UrlPatternMatchInput::Init(deno_urlpattern::UrlPatternInit { + ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit { protocol: init .get_item("protocol")? .map(|v| v.extract::()) @@ -205,13 +205,13 @@ impl UrlPattern { .transpose()? .map(|v| v.parse::()) .transpose() - .map_err(deno_urlpattern::Error::Url) + .map_err(::urlpattern::Error::Url) .map_err(Error)?, }) } }, - None => deno_urlpattern::UrlPatternMatchInput::Init( - deno_urlpattern::UrlPatternInit::default(), + None => ::urlpattern::UrlPatternMatchInput::Init( + ::urlpattern::UrlPatternInit::default(), ), }; Ok(self.0.test(input).map_err(Error)?) @@ -225,7 +225,7 @@ impl UrlPattern { baseURL: Option<&Bound<'py, PyString>>, ) -> PyResult>> { let urlpattern_input: Option = input.map(|i| i.extract()).transpose()?; - let input: deno_urlpattern::UrlPatternMatchInput = match &urlpattern_input { + let input: ::urlpattern::UrlPatternMatchInput = match &urlpattern_input { Some(input) => match input { UrlPatternInput::String(input) => match baseURL { Some(base_url) => { @@ -233,7 +233,7 @@ impl UrlPattern { Ok(url) => url, Err(_) => return Ok(None), }; - deno_urlpattern::UrlPatternMatchInput::Url( + ::urlpattern::UrlPatternMatchInput::Url( match url::Url::options() .base_url(Some(&base_url)) .parse(input.as_ref()) @@ -243,7 +243,7 @@ impl UrlPattern { }, ) } - None => deno_urlpattern::UrlPatternMatchInput::Url( + None => ::urlpattern::UrlPatternMatchInput::Url( match input.parse::() { Ok(url) => url, Err(_) => return Ok(None), @@ -255,7 +255,7 @@ impl UrlPattern { return Err(PyTypeError::new_err("cannot use dict input with baseURL")); } - deno_urlpattern::UrlPatternMatchInput::Init(deno_urlpattern::UrlPatternInit { + ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit { protocol: init .get_item("protocol")? .map(|v| v.extract::()) @@ -294,13 +294,13 @@ impl UrlPattern { .transpose()? .map(|v| v.parse::()) .transpose() - .map_err(deno_urlpattern::Error::Url) + .map_err(::urlpattern::Error::Url) .map_err(Error)?, }) } }, - None => deno_urlpattern::UrlPatternMatchInput::Init( - deno_urlpattern::UrlPatternInit::default(), + None => ::urlpattern::UrlPatternMatchInput::Init( + ::urlpattern::UrlPatternInit::default(), ), }; @@ -457,7 +457,7 @@ struct UrlPatternComponentResult { groups: HashMap>, } -struct Error(deno_urlpattern::Error); +struct Error(::urlpattern::Error); impl From for PyErr { fn from(error: Error) -> Self { @@ -465,8 +465,8 @@ impl From for PyErr { } } -impl From for Error { - fn from(other: deno_urlpattern::Error) -> Self { +impl From<::urlpattern::Error> for Error { + fn from(other: ::urlpattern::Error) -> Self { Self(other) } } From 7c610cf8599a827e4f6cfa2c0f384e4a9fcf4b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 14:20:04 +0000 Subject: [PATCH 72/98] Specify the library name in Cargo.toml --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 4df897c..5fd8cce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ categories = ["web-programming"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] +name = "urlpattern" crate-type = ["cdylib"] [dependencies] From 23e8a8a4665402c7d6c66c1e2ede97b5a79b0265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 15:01:09 +0000 Subject: [PATCH 73/98] Test `test` method --- tests/test_lib.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/test_lib.py b/tests/test_lib.py index fe0884f..94c8df7 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -40,23 +40,28 @@ def test(entry): assert getattr(pattern, key) == entry["expected_obj"][key] if entry.get("expected_match") == "error": + with pytest.raises(Exception): + pattern.test(*entry["inputs"]) + with pytest.raises(Exception): pattern.exec(*entry["inputs"]) + return - if isinstance(entry.get("expected_match"), dict): - result = pattern.exec(*entry["inputs"]) + elif isinstance(entry.get("expected_match"), dict): + assert pattern.test(*entry["inputs"]) + result = pattern.exec(*entry["inputs"]) for key in entry["expected_match"]: assert result[key] == entry["expected_match"][key] else: - result = pattern.exec(*entry["inputs"]) - assert result is None + assert not pattern.test(*entry["inputs"]) + + assert pattern.exec(*entry["inputs"]) is None if "exactly_empty_components" in entry: result = pattern.exec(*entry["inputs"]) - for component in entry["exactly_empty_components"]: if result: assert result[component]["groups"] == {} From 70befb176ffb63c1bba64f1c2468f3838486386c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 15:05:12 +0000 Subject: [PATCH 74/98] Refactor --- tests/test_lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_lib.py b/tests/test_lib.py index 94c8df7..12fc7d9 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -33,7 +33,8 @@ def test(entry): except UnicodeEncodeError as e: if e.reason == "surrogates not allowed": pytest.skip("unsupported in the implementation") - raise + else: + raise if "expected_obj" in entry: for key in entry["expected_obj"]: From b8fb90b7d23152196aff600ef42f4cf8412c9d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 16:00:54 +0000 Subject: [PATCH 75/98] Format --- src/lib.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 844a072..ad11d0d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -154,12 +154,12 @@ impl UrlPattern { }, ) } - None => ::urlpattern::UrlPatternMatchInput::Url( - match input.parse::() { + None => { + ::urlpattern::UrlPatternMatchInput::Url(match input.parse::() { Ok(url) => url, Err(_) => return Ok(false), - }, - ), + }) + } }, UrlPatternInput::Init(init) => { if let Some(_) = baseURL { @@ -210,9 +210,9 @@ impl UrlPattern { }) } }, - None => ::urlpattern::UrlPatternMatchInput::Init( - ::urlpattern::UrlPatternInit::default(), - ), + None => { + ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit::default()) + } }; Ok(self.0.test(input).map_err(Error)?) } @@ -243,12 +243,12 @@ impl UrlPattern { }, ) } - None => ::urlpattern::UrlPatternMatchInput::Url( - match input.parse::() { + None => { + ::urlpattern::UrlPatternMatchInput::Url(match input.parse::() { Ok(url) => url, Err(_) => return Ok(None), - }, - ), + }) + } }, UrlPatternInput::Init(init) => { if let Some(_) = baseURL { @@ -299,9 +299,9 @@ impl UrlPattern { }) } }, - None => ::urlpattern::UrlPatternMatchInput::Init( - ::urlpattern::UrlPatternInit::default(), - ), + None => { + ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit::default()) + } }; let Some(result) = self.0.exec(input).map_err(Error)? else { From 5958709b2bc3045364c345c91909e067ec68d8f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 16:02:04 +0000 Subject: [PATCH 76/98] Simplify `input` and `baseURL` validation --- src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ad11d0d..5a05849 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,10 +42,8 @@ impl UrlPattern { None => (None, options), }; - if let Some(UrlPatternInput::Init(_)) = input { - if let Some(_) = base_url { - return Err(PyTypeError::new_err("cannot use dict input with baseURL")); - } + if matches!(input, Some(UrlPatternInput::Init(_))) && base_url.is_some() { + return Err(PyTypeError::new_err("cannot use dict input with baseURL")); } let init: ::urlpattern::UrlPatternInit = match input { @@ -162,7 +160,7 @@ impl UrlPattern { } }, UrlPatternInput::Init(init) => { - if let Some(_) = baseURL { + if baseURL.is_some() { return Err(PyTypeError::new_err("cannot use dict input with baseURL")); } @@ -251,7 +249,7 @@ impl UrlPattern { } }, UrlPatternInput::Init(init) => { - if let Some(_) = baseURL { + if baseURL.is_some() { return Err(PyTypeError::new_err("cannot use dict input with baseURL")); } From d0ea7fafffaf4cbe9dff3576fad302e66611d3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 16:05:37 +0000 Subject: [PATCH 77/98] Convert `baseURL` to snake_case first --- src/lib.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5a05849..6870ca0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,9 @@ impl UrlPattern { baseURL: Option<&Bound<'_, PyAny>>, options: Option<&Bound<'_, PyDict>>, ) -> PyResult { - let (base_url, options) = match baseURL { + let base_url = baseURL; + + let (base_url, options) = match base_url { Some(value) => { if let Ok(options_dict) = value.cast::() { (None, Some(options_dict)) @@ -134,9 +136,11 @@ impl UrlPattern { #[pyo3(signature = (input=None, baseURL=None))] fn test(&self, input: Option, baseURL: Option<&str>) -> PyResult { + let base_url = baseURL; + let input: ::urlpattern::UrlPatternMatchInput = match input { Some(input) => match input { - UrlPatternInput::String(input) => match baseURL { + UrlPatternInput::String(input) => match base_url { Some(base_url) => { let base_url = match url::Url::parse(base_url) { Ok(url) => url, @@ -160,7 +164,7 @@ impl UrlPattern { } }, UrlPatternInput::Init(init) => { - if baseURL.is_some() { + if base_url.is_some() { return Err(PyTypeError::new_err("cannot use dict input with baseURL")); } @@ -222,10 +226,12 @@ impl UrlPattern { input: Option<&Bound<'py, PyAny>>, baseURL: Option<&Bound<'py, PyString>>, ) -> PyResult>> { + let base_url = baseURL; + let urlpattern_input: Option = input.map(|i| i.extract()).transpose()?; let input: ::urlpattern::UrlPatternMatchInput = match &urlpattern_input { Some(input) => match input { - UrlPatternInput::String(input) => match baseURL { + UrlPatternInput::String(input) => match base_url { Some(base_url) => { let base_url = match url::Url::parse(base_url.to_str()?) { Ok(url) => url, @@ -312,7 +318,7 @@ impl UrlPattern { vec.push( urlpattern_input.unwrap_or(UrlPatternInput::Init(PyDict::new(py).into_bound())), ); - if let Some(base_url) = baseURL { + if let Some(base_url) = base_url { vec.push(UrlPatternInput::String(base_url.to_string())); } vec From 90df45c0e7b478d56448f190aad93e57ce552469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 16:24:09 +0000 Subject: [PATCH 78/98] Refactor nested match --- src/lib.rs | 366 ++++++++++++++++++++++++++--------------------------- 1 file changed, 178 insertions(+), 188 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6870ca0..fc82090 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,56 +49,54 @@ impl UrlPattern { } let init: ::urlpattern::UrlPatternInit = match input { - Some(input) => match input { - UrlPatternInput::String(input) => { - ::urlpattern::UrlPatternInit::parse_constructor_string::( - input.as_str(), - base_url, - ) - .map_err(Error)? - } - UrlPatternInput::Init(init) => ::urlpattern::UrlPatternInit { - protocol: init - .get_item("protocol")? - .map(|v| v.extract::()) - .transpose()?, - username: init - .get_item("username")? - .map(|v| v.extract::()) - .transpose()?, - password: init - .get_item("password")? - .map(|v| v.extract::()) - .transpose()?, - hostname: init - .get_item("hostname")? - .map(|v| v.extract::()) - .transpose()?, - port: init - .get_item("port")? - .map(|v| v.extract::()) - .transpose()?, - pathname: init - .get_item("pathname")? - .map(|v| v.extract::()) - .transpose()?, - search: init - .get_item("search")? - .map(|v| v.extract::()) - .transpose()?, - hash: init - .get_item("hash")? - .map(|v| v.extract::()) - .transpose()?, - base_url: init - .get_item("baseURL")? - .map(|v| v.extract::()) - .transpose()? - .map(|v| v.parse::()) - .transpose() - .map_err(::urlpattern::Error::Url) - .map_err(Error)?, - }, + Some(UrlPatternInput::String(input)) => { + ::urlpattern::UrlPatternInit::parse_constructor_string::( + input.as_str(), + base_url, + ) + .map_err(Error)? + } + Some(UrlPatternInput::Init(init)) => ::urlpattern::UrlPatternInit { + protocol: init + .get_item("protocol")? + .map(|v| v.extract::()) + .transpose()?, + username: init + .get_item("username")? + .map(|v| v.extract::()) + .transpose()?, + password: init + .get_item("password")? + .map(|v| v.extract::()) + .transpose()?, + hostname: init + .get_item("hostname")? + .map(|v| v.extract::()) + .transpose()?, + port: init + .get_item("port")? + .map(|v| v.extract::()) + .transpose()?, + pathname: init + .get_item("pathname")? + .map(|v| v.extract::()) + .transpose()?, + search: init + .get_item("search")? + .map(|v| v.extract::()) + .transpose()?, + hash: init + .get_item("hash")? + .map(|v| v.extract::()) + .transpose()?, + base_url: init + .get_item("baseURL")? + .map(|v| v.extract::()) + .transpose()? + .map(|v| v.parse::()) + .transpose() + .map_err(::urlpattern::Error::Url) + .map_err(Error)?, }, None => ::urlpattern::UrlPatternInit::default(), }; @@ -139,79 +137,75 @@ impl UrlPattern { let base_url = baseURL; let input: ::urlpattern::UrlPatternMatchInput = match input { - Some(input) => match input { - UrlPatternInput::String(input) => match base_url { - Some(base_url) => { - let base_url = match url::Url::parse(base_url) { + Some(UrlPatternInput::String(input)) => match base_url { + Some(base_url) => { + let base_url = match url::Url::parse(base_url) { + Ok(url) => url, + Err(_) => return Ok(false), + }; + ::urlpattern::UrlPatternMatchInput::Url( + match url::Url::options() + .base_url(Some(&base_url)) + .parse(input.as_ref()) + { Ok(url) => url, Err(_) => return Ok(false), - }; - ::urlpattern::UrlPatternMatchInput::Url( - match url::Url::options() - .base_url(Some(&base_url)) - .parse(input.as_ref()) - { - Ok(url) => url, - Err(_) => return Ok(false), - }, - ) - } - None => { - ::urlpattern::UrlPatternMatchInput::Url(match input.parse::() { - Ok(url) => url, - Err(_) => return Ok(false), - }) - } - }, - UrlPatternInput::Init(init) => { - if base_url.is_some() { - return Err(PyTypeError::new_err("cannot use dict input with baseURL")); - } - - ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit { - protocol: init - .get_item("protocol")? - .map(|v| v.extract::()) - .transpose()?, - username: init - .get_item("username")? - .map(|v| v.extract::()) - .transpose()?, - password: init - .get_item("password")? - .map(|v| v.extract::()) - .transpose()?, - hostname: init - .get_item("hostname")? - .map(|v| v.extract::()) - .transpose()?, - port: init - .get_item("port")? - .map(|v| v.extract::()) - .transpose()?, - pathname: init - .get_item("pathname")? - .map(|v| v.extract::()) - .transpose()?, - search: init - .get_item("search")? - .map(|v| v.extract::()) - .transpose()?, - hash: init - .get_item("hash")? - .map(|v| v.extract::()) - .transpose()?, - base_url: init - .get_item("baseURL")? - .map(|v| v.extract::()) - .transpose()? - .map(|v| v.parse::()) - .transpose() - .map_err(::urlpattern::Error::Url) - .map_err(Error)?, - }) + }, + ) } + None => ::urlpattern::UrlPatternMatchInput::Url(match input.parse::() { + Ok(url) => url, + Err(_) => return Ok(false), + }), }, + Some(UrlPatternInput::Init(init)) => { + if base_url.is_some() { + return Err(PyTypeError::new_err("cannot use dict input with baseURL")); + } + + ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit { + protocol: init + .get_item("protocol")? + .map(|v| v.extract::()) + .transpose()?, + username: init + .get_item("username")? + .map(|v| v.extract::()) + .transpose()?, + password: init + .get_item("password")? + .map(|v| v.extract::()) + .transpose()?, + hostname: init + .get_item("hostname")? + .map(|v| v.extract::()) + .transpose()?, + port: init + .get_item("port")? + .map(|v| v.extract::()) + .transpose()?, + pathname: init + .get_item("pathname")? + .map(|v| v.extract::()) + .transpose()?, + search: init + .get_item("search")? + .map(|v| v.extract::()) + .transpose()?, + hash: init + .get_item("hash")? + .map(|v| v.extract::()) + .transpose()?, + base_url: init + .get_item("baseURL")? + .map(|v| v.extract::()) + .transpose()? + .map(|v| v.parse::()) + .transpose() + .map_err(::urlpattern::Error::Url) + .map_err(Error)?, + }) + } None => { ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit::default()) } @@ -230,79 +224,75 @@ impl UrlPattern { let urlpattern_input: Option = input.map(|i| i.extract()).transpose()?; let input: ::urlpattern::UrlPatternMatchInput = match &urlpattern_input { - Some(input) => match input { - UrlPatternInput::String(input) => match base_url { - Some(base_url) => { - let base_url = match url::Url::parse(base_url.to_str()?) { + Some(UrlPatternInput::String(input)) => match base_url { + Some(base_url) => { + let base_url = match url::Url::parse(base_url.to_str()?) { + Ok(url) => url, + Err(_) => return Ok(None), + }; + ::urlpattern::UrlPatternMatchInput::Url( + match url::Url::options() + .base_url(Some(&base_url)) + .parse(input.as_ref()) + { Ok(url) => url, Err(_) => return Ok(None), - }; - ::urlpattern::UrlPatternMatchInput::Url( - match url::Url::options() - .base_url(Some(&base_url)) - .parse(input.as_ref()) - { - Ok(url) => url, - Err(_) => return Ok(None), - }, - ) - } - None => { - ::urlpattern::UrlPatternMatchInput::Url(match input.parse::() { - Ok(url) => url, - Err(_) => return Ok(None), - }) - } - }, - UrlPatternInput::Init(init) => { - if baseURL.is_some() { - return Err(PyTypeError::new_err("cannot use dict input with baseURL")); - } - - ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit { - protocol: init - .get_item("protocol")? - .map(|v| v.extract::()) - .transpose()?, - username: init - .get_item("username")? - .map(|v| v.extract::()) - .transpose()?, - password: init - .get_item("password")? - .map(|v| v.extract::()) - .transpose()?, - hostname: init - .get_item("hostname")? - .map(|v| v.extract::()) - .transpose()?, - port: init - .get_item("port")? - .map(|v| v.extract::()) - .transpose()?, - pathname: init - .get_item("pathname")? - .map(|v| v.extract::()) - .transpose()?, - search: init - .get_item("search")? - .map(|v| v.extract::()) - .transpose()?, - hash: init - .get_item("hash")? - .map(|v| v.extract::()) - .transpose()?, - base_url: init - .get_item("baseURL")? - .map(|v| v.extract::()) - .transpose()? - .map(|v| v.parse::()) - .transpose() - .map_err(::urlpattern::Error::Url) - .map_err(Error)?, - }) + }, + ) } + None => ::urlpattern::UrlPatternMatchInput::Url(match input.parse::() { + Ok(url) => url, + Err(_) => return Ok(None), + }), }, + Some(UrlPatternInput::Init(init)) => { + if baseURL.is_some() { + return Err(PyTypeError::new_err("cannot use dict input with baseURL")); + } + + ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit { + protocol: init + .get_item("protocol")? + .map(|v| v.extract::()) + .transpose()?, + username: init + .get_item("username")? + .map(|v| v.extract::()) + .transpose()?, + password: init + .get_item("password")? + .map(|v| v.extract::()) + .transpose()?, + hostname: init + .get_item("hostname")? + .map(|v| v.extract::()) + .transpose()?, + port: init + .get_item("port")? + .map(|v| v.extract::()) + .transpose()?, + pathname: init + .get_item("pathname")? + .map(|v| v.extract::()) + .transpose()?, + search: init + .get_item("search")? + .map(|v| v.extract::()) + .transpose()?, + hash: init + .get_item("hash")? + .map(|v| v.extract::()) + .transpose()?, + base_url: init + .get_item("baseURL")? + .map(|v| v.extract::()) + .transpose()? + .map(|v| v.parse::()) + .transpose() + .map_err(::urlpattern::Error::Url) + .map_err(Error)?, + }) + } None => { ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit::default()) } From f2561d3fc3f9e6fe2ffc3ddbe4c962355c15c6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 16:28:01 +0000 Subject: [PATCH 79/98] Refactor `base_url` validation --- src/lib.rs | 94 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fc82090..f45596f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,10 +44,6 @@ impl UrlPattern { None => (None, options), }; - if matches!(input, Some(UrlPatternInput::Init(_))) && base_url.is_some() { - return Err(PyTypeError::new_err("cannot use dict input with baseURL")); - } - let init: ::urlpattern::UrlPatternInit = match input { Some(UrlPatternInput::String(input)) => { ::urlpattern::UrlPatternInit::parse_constructor_string::( @@ -56,48 +52,54 @@ impl UrlPattern { ) .map_err(Error)? } - Some(UrlPatternInput::Init(init)) => ::urlpattern::UrlPatternInit { - protocol: init - .get_item("protocol")? - .map(|v| v.extract::()) - .transpose()?, - username: init - .get_item("username")? - .map(|v| v.extract::()) - .transpose()?, - password: init - .get_item("password")? - .map(|v| v.extract::()) - .transpose()?, - hostname: init - .get_item("hostname")? - .map(|v| v.extract::()) - .transpose()?, - port: init - .get_item("port")? - .map(|v| v.extract::()) - .transpose()?, - pathname: init - .get_item("pathname")? - .map(|v| v.extract::()) - .transpose()?, - search: init - .get_item("search")? - .map(|v| v.extract::()) - .transpose()?, - hash: init - .get_item("hash")? - .map(|v| v.extract::()) - .transpose()?, - base_url: init - .get_item("baseURL")? - .map(|v| v.extract::()) - .transpose()? - .map(|v| v.parse::()) - .transpose() - .map_err(::urlpattern::Error::Url) - .map_err(Error)?, - }, + Some(UrlPatternInput::Init(init)) => { + if base_url.is_some() { + return Err(PyTypeError::new_err("cannot use dict input with baseURL")); + } + + ::urlpattern::UrlPatternInit { + protocol: init + .get_item("protocol")? + .map(|v| v.extract::()) + .transpose()?, + username: init + .get_item("username")? + .map(|v| v.extract::()) + .transpose()?, + password: init + .get_item("password")? + .map(|v| v.extract::()) + .transpose()?, + hostname: init + .get_item("hostname")? + .map(|v| v.extract::()) + .transpose()?, + port: init + .get_item("port")? + .map(|v| v.extract::()) + .transpose()?, + pathname: init + .get_item("pathname")? + .map(|v| v.extract::()) + .transpose()?, + search: init + .get_item("search")? + .map(|v| v.extract::()) + .transpose()?, + hash: init + .get_item("hash")? + .map(|v| v.extract::()) + .transpose()?, + base_url: init + .get_item("baseURL")? + .map(|v| v.extract::()) + .transpose()? + .map(|v| v.parse::()) + .transpose() + .map_err(::urlpattern::Error::Url) + .map_err(Error)?, + } + } None => ::urlpattern::UrlPatternInit::default(), }; let options = if let Some(options) = options { From e6224a94e68ef05d9103ff7ab55782e45d8f77ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 16:28:23 +0000 Subject: [PATCH 80/98] Refactor --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f45596f..ccee682 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -248,7 +248,7 @@ impl UrlPattern { }), }, Some(UrlPatternInput::Init(init)) => { - if baseURL.is_some() { + if base_url.is_some() { return Err(PyTypeError::new_err("cannot use dict input with baseURL")); } From a5e6e122c23da5e127d48f00dc71b5482826c7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 1 Apr 2026 16:35:16 +0000 Subject: [PATCH 81/98] Reorder --- src/lib.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ccee682..6fdde52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,19 @@ impl UrlPattern { None => (None, options), }; + let options = if let Some(options) = options { + ::urlpattern::UrlPatternOptions { + ignore_case: options + .get_item("ignoreCase")? + .map(|v| v.extract::()) + .transpose()? + .unwrap_or(false), + ..::urlpattern::UrlPatternOptions::default() + } + } else { + ::urlpattern::UrlPatternOptions::default() + }; + let init: ::urlpattern::UrlPatternInit = match input { Some(UrlPatternInput::String(input)) => { ::urlpattern::UrlPatternInit::parse_constructor_string::( @@ -102,18 +115,7 @@ impl UrlPattern { } None => ::urlpattern::UrlPatternInit::default(), }; - let options = if let Some(options) = options { - ::urlpattern::UrlPatternOptions { - ignore_case: options - .get_item("ignoreCase")? - .map(|v| v.extract::()) - .transpose()? - .unwrap_or(false), - ..::urlpattern::UrlPatternOptions::default() - } - } else { - ::urlpattern::UrlPatternOptions::default() - }; + Ok(Self( ::urlpattern::UrlPattern::parse(init, options).map_err(Error)?, )) @@ -212,6 +214,7 @@ impl UrlPattern { ::urlpattern::UrlPatternMatchInput::Init(::urlpattern::UrlPatternInit::default()) } }; + Ok(self.0.test(input).map_err(Error)?) } From 9024c83b2a7af2d873df3000dfaaaf034741377a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Thu, 2 Apr 2026 15:07:08 +0000 Subject: [PATCH 82/98] Refactor `base_url` extraction logic --- src/lib.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6fdde52..972aa6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,21 +29,24 @@ impl UrlPattern { } else if value.is_none() { (None, options) } else { - ( - Some( - value - .extract::()? - .parse::() - .map_err(::urlpattern::Error::Url) - .map_err(Error)?, - ), - options, - ) + (base_url, options) } } None => (None, options), }; + let base_url = if let Some(base_url) = base_url { + Some( + base_url + .extract::()? + .parse::() + .map_err(::urlpattern::Error::Url) + .map_err(Error)?, + ) + } else { + None + }; + let options = if let Some(options) = options { ::urlpattern::UrlPatternOptions { ignore_case: options From bde33a5eba5065f441def41e3fa162ad4f30fb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 4 Apr 2026 20:04:16 +0900 Subject: [PATCH 83/98] Remove unnecessary `as_ref()` --- src/lib.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 972aa6b..abb0280 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,10 +151,7 @@ impl UrlPattern { Err(_) => return Ok(false), }; ::urlpattern::UrlPatternMatchInput::Url( - match url::Url::options() - .base_url(Some(&base_url)) - .parse(input.as_ref()) - { + match url::Url::options().base_url(Some(&base_url)).parse(&input) { Ok(url) => url, Err(_) => return Ok(false), }, @@ -239,10 +236,7 @@ impl UrlPattern { Err(_) => return Ok(None), }; ::urlpattern::UrlPatternMatchInput::Url( - match url::Url::options() - .base_url(Some(&base_url)) - .parse(input.as_ref()) - { + match url::Url::options().base_url(Some(&base_url)).parse(input) { Ok(url) => url, Err(_) => return Ok(None), }, From dedbd1b09e61035d69cbccd78d6bcafff3606aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 4 Apr 2026 20:11:24 +0900 Subject: [PATCH 84/98] Refactor `if let` to `match` --- src/lib.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index abb0280..51e5b86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,29 +35,27 @@ impl UrlPattern { None => (None, options), }; - let base_url = if let Some(base_url) = base_url { - Some( + let base_url = match base_url { + Some(base_url) => Some( base_url .extract::()? .parse::() .map_err(::urlpattern::Error::Url) .map_err(Error)?, - ) - } else { - None + ), + None => None, }; - let options = if let Some(options) = options { - ::urlpattern::UrlPatternOptions { + let options = match options { + Some(options) => ::urlpattern::UrlPatternOptions { ignore_case: options .get_item("ignoreCase")? .map(|v| v.extract::()) .transpose()? .unwrap_or(false), ..::urlpattern::UrlPatternOptions::default() - } - } else { - ::urlpattern::UrlPatternOptions::default() + }, + None => ::urlpattern::UrlPatternOptions::default(), }; let init: ::urlpattern::UrlPatternInit = match input { From 1401d862649042a5ced8f27800c550992a57bae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 4 Apr 2026 21:30:26 +0900 Subject: [PATCH 85/98] Move UrlPatternInput --- src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 51e5b86..35f502b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,12 @@ use pyo3::{ }; use std::collections::HashMap; +#[derive(FromPyObject)] +enum UrlPatternInput<'py> { + String(String), + Init(Bound<'py, PyDict>), +} + #[pyclass(name = "URLPattern")] struct UrlPattern(::urlpattern::UrlPattern); @@ -394,12 +400,6 @@ impl UrlPattern { } } -#[derive(FromPyObject)] -enum UrlPatternInput<'py> { - String(String), - Init(Bound<'py, PyDict>), -} - struct UrlPatternResult<'py> { inputs: Vec>, protocol: UrlPatternComponentResult, From 4437fe8aab3426a88b71999d8ab04aa7d8127b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 4 Apr 2026 21:48:46 +0900 Subject: [PATCH 86/98] Remove `unwrap()` --- src/lib.rs | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 35f502b..f4583bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,19 +128,18 @@ impl UrlPattern { )) } - fn __repr__(&self, py: Python) -> String { + fn __repr__(&self, py: Python) -> PyResult { let dict = PyDict::new(py); - dict.set_item("protocol", self.0.protocol()).unwrap(); - dict.set_item("username", self.0.username()).unwrap(); - dict.set_item("password", self.0.password()).unwrap(); - dict.set_item("hostname", self.0.hostname()).unwrap(); - dict.set_item("port", self.0.port()).unwrap(); - dict.set_item("pathname", self.0.pathname()).unwrap(); - dict.set_item("search", self.0.search()).unwrap(); - dict.set_item("hash", self.0.hash()).unwrap(); - dict.set_item("hasRegExpGroups", self.0.has_regexp_groups()) - .unwrap(); - format!("URLPattern({})", dict) + dict.set_item("protocol", self.0.protocol())?; + dict.set_item("username", self.0.username())?; + dict.set_item("password", self.0.password())?; + dict.set_item("hostname", self.0.hostname())?; + dict.set_item("port", self.0.port())?; + dict.set_item("pathname", self.0.pathname())?; + dict.set_item("search", self.0.search())?; + dict.set_item("hash", self.0.hash())?; + dict.set_item("hasRegExpGroups", self.0.has_regexp_groups())?; + Ok(format!("URLPattern({})", dict)) } #[pyo3(signature = (input=None, baseURL=None))] @@ -415,7 +414,7 @@ struct UrlPatternResult<'py> { impl<'py> IntoPyObject<'py> for UrlPatternResult<'py> { type Target = PyDict; type Output = Bound<'py, Self::Target>; - type Error = std::convert::Infallible; + type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); @@ -424,23 +423,23 @@ impl<'py> IntoPyObject<'py> for UrlPatternResult<'py> { for input in self.inputs { match input { UrlPatternInput::String(string) => { - inputs.append(string).unwrap(); + inputs.append(string)?; } UrlPatternInput::Init(init) => { - inputs.append(init).unwrap(); + inputs.append(init)?; } } } - dict.set_item("inputs", inputs).unwrap(); - dict.set_item("protocol", self.protocol).unwrap(); - dict.set_item("username", self.username).unwrap(); - dict.set_item("password", self.password).unwrap(); - dict.set_item("hostname", self.hostname).unwrap(); - dict.set_item("port", self.port).unwrap(); - dict.set_item("pathname", self.pathname).unwrap(); - dict.set_item("search", self.search).unwrap(); - dict.set_item("hash", self.hash).unwrap(); + dict.set_item("inputs", inputs)?; + dict.set_item("protocol", self.protocol)?; + dict.set_item("username", self.username)?; + dict.set_item("password", self.password)?; + dict.set_item("hostname", self.hostname)?; + dict.set_item("port", self.port)?; + dict.set_item("pathname", self.pathname)?; + dict.set_item("search", self.search)?; + dict.set_item("hash", self.hash)?; Ok(dict.into_bound()) } } From b9d24027e5141d6be3f90f09ac722ce05797710d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 4 Apr 2026 23:44:15 +0900 Subject: [PATCH 87/98] Inline the contents of `UrlPatternResult` --- src/lib.rs | 158 +++++++++++++++++++---------------------------------- 1 file changed, 56 insertions(+), 102 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f4583bf..5d8bd2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,10 @@ #![allow(non_snake_case)] use pyo3::{ - BoundObject, exceptions::{PyTypeError, PyValueError}, prelude::*, types::{PyDict, PyList, PyString}, }; -use std::collections::HashMap; #[derive(FromPyObject)] enum UrlPatternInput<'py> { @@ -227,11 +225,13 @@ impl UrlPattern { py: Python<'py>, input: Option<&Bound<'py, PyAny>>, baseURL: Option<&Bound<'py, PyString>>, - ) -> PyResult>> { + ) -> PyResult>> { let base_url = baseURL; - let urlpattern_input: Option = input.map(|i| i.extract()).transpose()?; - let input: ::urlpattern::UrlPatternMatchInput = match &urlpattern_input { + let urlpattern_input: ::urlpattern::UrlPatternMatchInput = match input + .map(|i| i.extract()) + .transpose()? + { Some(UrlPatternInput::String(input)) => match base_url { Some(base_url) => { let base_url = match url::Url::parse(base_url.to_str()?) { @@ -239,7 +239,7 @@ impl UrlPattern { Err(_) => return Ok(None), }; ::urlpattern::UrlPatternMatchInput::Url( - match url::Url::options().base_url(Some(&base_url)).parse(input) { + match url::Url::options().base_url(Some(&base_url)).parse(&input) { Ok(url) => url, Err(_) => return Ok(None), }, @@ -303,54 +303,59 @@ impl UrlPattern { } }; - let Some(result) = self.0.exec(input).map_err(Error)? else { + let Some(urlpattern_result) = self.0.exec(urlpattern_input).map_err(Error)? else { return Ok(None); }; - Ok(Some(UrlPatternResult { - inputs: { - let mut vec = Vec::new(); - vec.push( - urlpattern_input.unwrap_or(UrlPatternInput::Init(PyDict::new(py).into_bound())), - ); - if let Some(base_url) = base_url { - vec.push(UrlPatternInput::String(base_url.to_string())); - } - vec - }, - protocol: UrlPatternComponentResult { - input: result.protocol.input, - groups: result.protocol.groups, - }, - username: UrlPatternComponentResult { - input: result.username.input, - groups: result.username.groups, - }, - password: UrlPatternComponentResult { - input: result.password.input, - groups: result.password.groups, - }, - hostname: UrlPatternComponentResult { - input: result.hostname.input, - groups: result.hostname.groups, - }, - port: UrlPatternComponentResult { - input: result.port.input, - groups: result.port.groups, - }, - pathname: UrlPatternComponentResult { - input: result.pathname.input, - groups: result.pathname.groups, - }, - search: UrlPatternComponentResult { - input: result.search.input, - groups: result.search.groups, - }, - hash: UrlPatternComponentResult { - input: result.hash.input, - groups: result.hash.groups, - }, - })) + let result = PyDict::new(py); + + let inputs = PyList::new(py, vec![input.unwrap_or(&PyDict::new(py))])?; + if let Some(base_url) = base_url { + inputs.append(base_url)?; + } + result.set_item("inputs", inputs)?; + + let protocol = PyDict::new(py); + protocol.set_item("input", urlpattern_result.protocol.input)?; + protocol.set_item("groups", urlpattern_result.protocol.groups)?; + result.set_item("protocol", protocol)?; + + let username = PyDict::new(py); + username.set_item("input", urlpattern_result.username.input)?; + username.set_item("groups", urlpattern_result.username.groups)?; + result.set_item("username", username)?; + + let password = PyDict::new(py); + password.set_item("input", urlpattern_result.password.input)?; + password.set_item("groups", urlpattern_result.password.groups)?; + result.set_item("password", password)?; + + let hostname = PyDict::new(py); + hostname.set_item("input", urlpattern_result.hostname.input)?; + hostname.set_item("groups", urlpattern_result.hostname.groups)?; + result.set_item("hostname", hostname)?; + + let port = PyDict::new(py); + port.set_item("input", urlpattern_result.port.input)?; + port.set_item("groups", urlpattern_result.port.groups)?; + result.set_item("port", port)?; + + let pathname = PyDict::new(py); + pathname.set_item("input", urlpattern_result.pathname.input)?; + pathname.set_item("groups", urlpattern_result.pathname.groups)?; + result.set_item("pathname", pathname)?; + + let search = PyDict::new(py); + search.set_item("input", urlpattern_result.search.input)?; + search.set_item("groups", urlpattern_result.search.groups)?; + result.set_item("search", search)?; + + let hash = PyDict::new(py); + hash.set_item("input", urlpattern_result.hash.input)?; + hash.set_item("groups", urlpattern_result.hash.groups)?; + result.set_item("hash", hash)?; + + Ok(Some(result)) } #[getter] @@ -399,57 +404,6 @@ impl UrlPattern { } } -struct UrlPatternResult<'py> { - inputs: Vec>, - protocol: UrlPatternComponentResult, - username: UrlPatternComponentResult, - password: UrlPatternComponentResult, - hostname: UrlPatternComponentResult, - port: UrlPatternComponentResult, - pathname: UrlPatternComponentResult, - search: UrlPatternComponentResult, - hash: UrlPatternComponentResult, -} - -impl<'py> IntoPyObject<'py> for UrlPatternResult<'py> { - type Target = PyDict; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let dict = PyDict::new(py); - - let inputs = PyList::empty(py); - for input in self.inputs { - match input { - UrlPatternInput::String(string) => { - inputs.append(string)?; - } - UrlPatternInput::Init(init) => { - inputs.append(init)?; - } - } - } - - dict.set_item("inputs", inputs)?; - dict.set_item("protocol", self.protocol)?; - dict.set_item("username", self.username)?; - dict.set_item("password", self.password)?; - dict.set_item("hostname", self.hostname)?; - dict.set_item("port", self.port)?; - dict.set_item("pathname", self.pathname)?; - dict.set_item("search", self.search)?; - dict.set_item("hash", self.hash)?; - Ok(dict.into_bound()) - } -} - -#[derive(IntoPyObject, IntoPyObjectRef)] -struct UrlPatternComponentResult { - input: String, - groups: HashMap>, -} - struct Error(::urlpattern::Error); impl From for PyErr { From c4c1885e71277ad7ffe431d060a278740cd28eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sun, 5 Apr 2026 00:02:01 +0900 Subject: [PATCH 88/98] Remove `github.vscode-github-actions` from recommendations --- .vscode/extensions.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index fbed416..169c237 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { "recommendations": [ "charliermarsh.ruff", - "github.vscode-github-actions", "ms-python.python", "redhat.vscode-yaml", "rust-lang.rust-analyzer", From 18fc33801b8119e1562c731317a8f8e9b82cb203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 6 Apr 2026 18:25:11 +0900 Subject: [PATCH 89/98] Fix `groups` type in `URLPatternComponentResult` to `Optional[str]` --- urlpattern.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/urlpattern.pyi b/urlpattern.pyi index 80ce214..73d4c32 100644 --- a/urlpattern.pyi +++ b/urlpattern.pyi @@ -104,6 +104,6 @@ class URLPatternResult(TypedDict): class URLPatternComponentResult(TypedDict): input: str - groups: dict[str, str] + groups: dict[str, Optional[str]] URLPatternCompatible: TypeAlias = Union[str, URLPatternInit, URLPattern] From 7b2d7005b697602ed408713337f8d31e5e5983e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Mon, 6 Apr 2026 18:27:02 +0900 Subject: [PATCH 90/98] Add an assertion to resolve a typing error --- tests/test_lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_lib.py b/tests/test_lib.py index 12fc7d9..c34ee56 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -53,6 +53,7 @@ def test(entry): assert pattern.test(*entry["inputs"]) result = pattern.exec(*entry["inputs"]) + assert result is not None for key in entry["expected_match"]: assert result[key] == entry["expected_match"][key] From 78dd631bb961c51d2fa51820e9adc4a08d4fb383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Tue, 7 Apr 2026 19:10:06 +0900 Subject: [PATCH 91/98] Refactor base_url handling --- src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5d8bd2b..1bbd526 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,13 +27,11 @@ impl UrlPattern { let base_url = baseURL; let (base_url, options) = match base_url { - Some(value) => { - if let Ok(options_dict) = value.cast::() { - (None, Some(options_dict)) - } else if value.is_none() { - (None, options) + Some(base_url) => { + if let Ok(options) = base_url.cast::() { + (None, Some(options)) } else { - (base_url, options) + (Some(base_url), options) } } None => (None, options), From 9167fc0a1ffd8091dd0a6c81078dac1f6ded5fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Tue, 7 Apr 2026 22:31:13 +0900 Subject: [PATCH 92/98] Clarify naming conventions --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 02af0a7..21d1791 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ The URL Pattern Standard is a web standard for URL pattern matching. It is usefu It's a thin wrapper of [denoland/rust-urlpattern](https://github.com/denoland/rust-urlpattern) with [PyO3](https://github.com/PyO3/pyo3) + [Maturin](https://github.com/PyO3/maturin). -The naming conventions follow [the standard](https://urlpattern.spec.whatwg.org/) as closely as possible, similar to [xml.dom](https://docs.python.org/3/library/xml.dom.html). - ## Installation On Linux/UNIX or macOS: @@ -128,6 +126,6 @@ Check `pytest.skip` in [`tests/test_lib.py`](https://github.com/urlpattern/pytho ## Why camelCase? -In this library, some names such as `baseURL` and `hasRegExpGroups` do not use snake_case. +As seen in names like `baseURL` and `hasRegExpGroups`, this library does not follow Python's [PEP 8 naming conventions](https://peps.python.org/pep-0008/#function-and-variable-names). Instead, it follows [the standard](https://urlpattern.spec.whatwg.org/) naming as closely as possible. -Like [`xml.dom`](https://docs.python.org/3/library/xml.dom.html), Python wrappers around web standards typically preserve the original camelCase rather than converting names to snake_case. This library follows that convention as well. +Like [`xml.dom`](https://docs.python.org/3/library/xml.dom.html), Python wrappers around web standards typically preserve the original camelCase rather than converting names to snake_case, and this library follows that convention as well. From 2842236aee7f87e6d4de9e3885a43ef2ecfd647b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Tue, 7 Apr 2026 22:35:44 +0900 Subject: [PATCH 93/98] Reorder introduction text for clarity --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 21d1791..e6e226d 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ An implementation of [the URL Pattern Standard](https://urlpattern.spec.whatwg.o ## Introduction -The URL Pattern Standard is a web standard for URL pattern matching. It is useful on the server side when serving different pages based on the URL (a.k.a. routing). It provides pattern matching syntax like `/users/:id`, similar to [route parameters in Express](https://expressjs.com/en/guide/routing.html#route-parameters) or [Path-to-RegExp](https://github.com/pillarjs/path-to-regexp). You can use it as a foundation to build your own web server or framework. - It's a thin wrapper of [denoland/rust-urlpattern](https://github.com/denoland/rust-urlpattern) with [PyO3](https://github.com/PyO3/pyo3) + [Maturin](https://github.com/PyO3/maturin). +It is useful on the server side when serving different pages based on the URL (a.k.a. routing). It provides pattern matching syntax like `/users/:id`, similar to [route parameters in Express](https://expressjs.com/en/guide/routing.html#route-parameters) or [Path-to-RegExp](https://github.com/pillarjs/path-to-regexp). You can use it as a foundation to build your own web server or framework. + ## Installation On Linux/UNIX or macOS: From 2e5f490866850eec56ec870f7acba976b377642f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Wed, 8 Apr 2026 18:30:20 +0900 Subject: [PATCH 94/98] Rename `urlpatterntestdata` to `URLPATTERNTESTDATA` --- tests/test_lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_lib.py b/tests/test_lib.py index c34ee56..526b52b 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -12,12 +12,12 @@ # 1. Go to https://github.com/web-platform-tests/wpt/blob/master/urlpattern/resources/urlpatterntestdata.json. # 2. Copy the content. # 3. Paste into `tests/urlpatterntestdata.json`. -urlpatterntestdata = json.loads( +URLPATTERNTESTDATA = json.loads( pathlib.Path("tests/urlpatterntestdata.json").read_text("utf-8") ) -@pytest.mark.parametrize("entry", urlpatterntestdata) +@pytest.mark.parametrize("entry", URLPATTERNTESTDATA) def test(entry): if entry["pattern"] == [{"pathname": "*{}**?"}] or entry["pattern"] == ["((?R)):"]: pytest.skip("unsupported in the implementation") From 4ea5283171031859a33edaf2a318cd7bcce2465f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 11 Apr 2026 00:01:09 +0900 Subject: [PATCH 95/98] Refactor Usage section --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e6e226d..2e49402 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,9 @@ py -m pip install urlpattern ## Usage -Check [urlpattern.pyi](https://github.com/urlpattern/python-urlpattern/blob/main/urlpattern.pyi) or the examples below. +This library aims to expose an interface as close as possible to the URL Pattern Standard, but some differences are unavoidable because it is designed for JavaScript, not Python. For the exact details, please refer to [urlpattern.pyi](https://github.com/urlpattern/python-urlpattern/blob/main/urlpattern.pyi). -For various usage examples, you can also check [Chrome for Developers](https://developer.chrome.com/docs/web-platform/urlpattern) or [MDN](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) (you need to convert JavaScript into Python). - -## Examples +Most JavaScript examples from [Chrome for Developers](https://developer.chrome.com/docs/web-platform/urlpattern) and [MDN](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) can be adapted to Python without much difficulty. ### `test` From 97da648a4406f02d53488ce1572e30df57207586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 11 Apr 2026 00:02:24 +0900 Subject: [PATCH 96/98] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e49402..9ec1faa 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ py -m pip install urlpattern ## Usage -This library aims to expose an interface as close as possible to the URL Pattern Standard, but some differences are unavoidable because it is designed for JavaScript, not Python. For the exact details, please refer to [urlpattern.pyi](https://github.com/urlpattern/python-urlpattern/blob/main/urlpattern.pyi). +This library aims to expose an interface as close as possible to the URL Pattern Standard, but some differences are unavoidable because it is designed for Python, not JavaScript. For the exact details, please refer to [urlpattern.pyi](https://github.com/urlpattern/python-urlpattern/blob/main/urlpattern.pyi). Most JavaScript examples from [Chrome for Developers](https://developer.chrome.com/docs/web-platform/urlpattern) and [MDN](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) can be adapted to Python without much difficulty. From fc6a542dc381f27f7f2e03d9ec4434d586f93596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 11 Apr 2026 00:41:08 +0900 Subject: [PATCH 97/98] Move outputs to the bottom of each code block --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9ec1faa..5bbdbb9 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,15 @@ Most JavaScript examples from [Chrome for Developers](https://developer.chrome.c from urlpattern import URLPattern pattern = URLPattern("https://example.com/admin/*") -print(pattern.test("https://example.com/admin/main/")) # output: True -print(pattern.test("https://example.com/main/")) # output: False +print(pattern.test("https://example.com/admin/main/")) +print(pattern.test("https://example.com/main/")) +``` + +Output: + +``` +True +False ``` ### `exec` @@ -50,7 +57,13 @@ from urlpattern import URLPattern pattern = URLPattern({"pathname": "/users/:id/"}) result = pattern.exec({"pathname": "/users/4163/"}) -print(result["pathname"]["groups"]["id"]) # output: 4163 +print(result["pathname"]["groups"]["id"]) +``` + +Output: + +``` +4163 ``` ### `baseURL` @@ -59,11 +72,19 @@ print(result["pathname"]["groups"]["id"]) # output: 4163 from urlpattern import URLPattern pattern = URLPattern("b", "https://example.com/a/") -print(pattern.test("a/b", "https://example.com/")) # output: True -print(pattern.test("b", "https://example.com/a/")) # output: True +print(pattern.test("a/b", "https://example.com/")) +print(pattern.test("b", "https://example.com/a/")) print( pattern.test({"pathname": "b", "baseURL": "https://example.com/a/"}) -) # output: True +) +``` + +Output: + +``` +True +True +True ``` ### `ignoreCase` @@ -72,12 +93,21 @@ print( from urlpattern import URLPattern pattern = URLPattern("https://example.com/test") -print(pattern.test("https://example.com/test")) # output: True -print(pattern.test("https://example.com/TeST")) # output: False +print(pattern.test("https://example.com/test")) +print(pattern.test("https://example.com/TeST")) pattern = URLPattern("https://example.com/test", {"ignoreCase": True}) -print(pattern.test("https://example.com/test")) # output: True -print(pattern.test("https://example.com/TeST")) # output: True +print(pattern.test("https://example.com/test")) +print(pattern.test("https://example.com/TeST")) +``` + +Output: + +``` +True +False +True +True ``` ### A simple WSGI app From 32fe7230e4497f7a9a96ef6d84d2fdfbf97bc51e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=A9=EC=84=B1=EB=B2=94=20=28Bang=20Seongbeom=29?= Date: Sat, 11 Apr 2026 01:48:02 +0900 Subject: [PATCH 98/98] Fix CI badge link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5bbdbb9..0c8204a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI - Version](https://img.shields.io/pypi/v/urlpattern)](https://pypi.org/project/urlpattern/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/urlpattern)](https://pypi.org/project/urlpattern/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![CI](https://github.com/urlpattern/python-urlpattern/actions/workflows/CI.yml/badge.svg)](https://github.com/urlpattern/python-urlpattern/actions) +[![CI](https://github.com/urlpattern/python-urlpattern/actions/workflows/CI.yml/badge.svg)](https://github.com/urlpattern/python-urlpattern/actions/workflows/CI.yml) An implementation of [the URL Pattern Standard](https://urlpattern.spec.whatwg.org/) for Python written in Rust.