Skip to content

Commit 6cc77f4

Browse files
committed
Path parser refactor
1 parent ec7ef3a commit 6cc77f4

File tree

3 files changed

+57
-33
lines changed

3 files changed

+57
-33
lines changed
Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,66 @@
1-
from typing import Any
1+
import re
2+
from dataclasses import dataclass
23

3-
from parse import Parser
44

5+
@dataclass(frozen=True)
6+
class PathMatchResult:
7+
"""Result of path parsing."""
8+
named: dict[str, str]
59

6-
class PathParameter:
7-
name = "PathParameter"
8-
pattern = r"[^\/]*"
910

10-
def __call__(self, text: str) -> str:
11-
return text
12-
13-
14-
class PathParser(Parser): # type: ignore
15-
16-
parse_path_parameter = PathParameter()
11+
class PathParser:
12+
"""Parses path patterns with parameters into regex and matches against URLs."""
13+
_PARAM_PATTERN = r"[^/]*"
1714

1815
def __init__(
1916
self, pattern: str, pre_expression: str = "", post_expression: str = ""
2017
) -> None:
21-
extra_types = {
22-
self.parse_path_parameter.name: self.parse_path_parameter
23-
}
24-
super().__init__(pattern, extra_types)
25-
self._expression: str = (
26-
pre_expression + self._expression + post_expression
27-
)
18+
self.pattern = pattern
19+
self._group_to_name: dict[str, str] = {}
20+
21+
regex_body = self._compile_template_to_regex(pattern)
22+
self._expression = f"{pre_expression}{regex_body}{post_expression}"
23+
self._compiled = re.compile(self._expression)
24+
25+
def search(self, text: str) -> PathMatchResult | None:
26+
"""Searches for a match in the given text."""
27+
match = self._compiled.search(text)
28+
return self._to_result(match)
2829

29-
def _handle_field(self, field: str) -> Any:
30-
# handle as path parameter field
31-
field = field[1:-1]
32-
path_parameter_field = "{%s:PathParameter}" % field
33-
return super()._handle_field(path_parameter_field)
30+
def parse(self, text: str) -> PathMatchResult | None:
31+
"""Parses the entire text for a match."""
32+
match = self._compiled.fullmatch(text)
33+
return self._to_result(match)
34+
35+
def _compile_template_to_regex(self, template: str) -> str:
36+
parts: list[str] = []
37+
i = 0
38+
group_index = 0
39+
while i < len(template):
40+
start = template.find("{", i)
41+
if start == -1:
42+
parts.append(re.escape(template[i:]))
43+
break
44+
end = template.find("}", start + 1)
45+
if end == -1:
46+
raise ValueError(f"Unmatched '{{' in template: {template!r}")
47+
48+
parts.append(re.escape(template[i:start]))
49+
param_name = template[start + 1 : end]
50+
group_name = f"g{group_index}"
51+
group_index += 1
52+
self._group_to_name[group_name] = param_name
53+
parts.append(f"(?P<{group_name}>{self._PARAM_PATTERN})")
54+
i = end + 1
55+
56+
return "".join(parts)
57+
58+
def _to_result(self, match: re.Match[str] | None) -> PathMatchResult | None:
59+
if match is None:
60+
return None
61+
return PathMatchResult(
62+
named={
63+
param_name: match.group(group_name)
64+
for group_name, param_name in self._group_to_name.items()
65+
},
66+
)

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ module = [
2121
"isodate.*",
2222
"jsonschema.*",
2323
"more_itertools.*",
24-
"parse.*",
2524
"requests.*",
2625
"werkzeug.*",
2726
]
@@ -69,7 +68,6 @@ aiohttp = {version = ">=3.0", optional = true}
6968
starlette = {version = ">=0.26.1,<0.50.0", optional = true}
7069
isodate = "*"
7170
more-itertools = "*"
72-
parse = "*"
7371
openapi-schema-validator = "^0.6.0"
7472
openapi-spec-validator = "^0.7.1"
7573
requests = {version = "*", optional = true}

tests/unit/templating/test_paths_parsers.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,6 @@ def test_chars_valid(self, path_pattern, expected):
4242

4343
assert result.named == expected
4444

45-
@pytest.mark.xfail(
46-
reason=(
47-
"Special characters of regex not supported. "
48-
"See https://github.com/python-openapi/openapi-core/issues/672"
49-
),
50-
strict=True,
51-
)
5245
@pytest.mark.parametrize(
5346
"path_pattern,expected",
5447
[

0 commit comments

Comments
 (0)