Skip to content

Commit 8005605

Browse files
committed
Path parser refactor
1 parent ec7ef3a commit 8005605

File tree

4 files changed

+61
-64
lines changed

4 files changed

+61
-64
lines changed
Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,72 @@
1-
from typing import Any
1+
from __future__ import annotations
22

3-
from parse import Parser
3+
import re
4+
from dataclasses import dataclass
45

56

6-
class PathParameter:
7-
name = "PathParameter"
8-
pattern = r"[^\/]*"
7+
@dataclass(frozen=True)
8+
class PathMatchResult:
9+
"""Result of path parsing."""
910

10-
def __call__(self, text: str) -> str:
11-
return text
11+
named: dict[str, str]
1212

1313

14-
class PathParser(Parser): # type: ignore
14+
class PathParser:
15+
"""Parses path patterns with parameters into regex and matches against URLs."""
1516

16-
parse_path_parameter = PathParameter()
17+
_PARAM_PATTERN = r"[^/]*"
1718

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

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

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)