Skip to content

Commit 81c620a

Browse files
committed
Path parser refactor
1 parent ec7ef3a commit 81c620a

File tree

4 files changed

+60
-65
lines changed

4 files changed

+60
-65
lines changed
Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,70 @@
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."""
58

6-
class PathParameter:
7-
name = "PathParameter"
8-
pattern = r"[^\/]*"
9+
named: dict[str, str]
910

10-
def __call__(self, text: str) -> str:
11-
return text
1211

12+
class PathParser:
13+
"""Parses path patterns with parameters into regex and matches against URLs."""
1314

14-
class PathParser(Parser): # type: ignore
15-
16-
parse_path_parameter = PathParameter()
15+
_PARAM_PATTERN = r"[^/]*"
1716

1817
def __init__(
1918
self, pattern: str, pre_expression: str = "", post_expression: str = ""
2019
) -> 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-
)
20+
self.pattern = pattern
21+
self._group_to_name: dict[str, str] = {}
22+
23+
regex_body = self._compile_template_to_regex(pattern)
24+
self._expression = f"{pre_expression}{regex_body}{post_expression}"
25+
self._compiled = re.compile(self._expression)
26+
27+
def search(self, text: str) -> PathMatchResult | None:
28+
"""Searches for a match in the given text."""
29+
match = self._compiled.search(text)
30+
return self._to_result(match)
31+
32+
def parse(self, text: str) -> PathMatchResult | None:
33+
"""Parses the entire text for a match."""
34+
match = self._compiled.fullmatch(text)
35+
return self._to_result(match)
2836

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)
37+
def _compile_template_to_regex(self, template: str) -> str:
38+
parts: list[str] = []
39+
i = 0
40+
group_index = 0
41+
while i < len(template):
42+
start = template.find("{", i)
43+
if start == -1:
44+
parts.append(re.escape(template[i:]))
45+
break
46+
end = template.find("}", start + 1)
47+
if end == -1:
48+
raise ValueError(f"Unmatched '{{' in template: {template!r}")
49+
50+
parts.append(re.escape(template[i:start]))
51+
param_name = template[start + 1 : end]
52+
group_name = f"g{group_index}"
53+
group_index += 1
54+
self._group_to_name[group_name] = param_name
55+
parts.append(f"(?P<{group_name}>{self._PARAM_PATTERN})")
56+
i = end + 1
57+
58+
return "".join(parts)
59+
60+
def _to_result(
61+
self, match: re.Match[str] | None
62+
) -> PathMatchResult | None:
63+
if match is None:
64+
return None
65+
return PathMatchResult(
66+
named={
67+
param_name: match.group(group_name)
68+
for group_name, param_name in self._group_to_name.items()
69+
},
70+
)

poetry.lock

Lines changed: 1 addition & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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)