Skip to content

Commit af136c2

Browse files
committed
Path parser refactor
1 parent ec7ef3a commit af136c2

File tree

4 files changed

+62
-64
lines changed

4 files changed

+62
-64
lines changed
Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,73 @@
1-
from typing import Any
1+
# Allow writing union types as X | Y in Python 3.9
2+
from __future__ import annotations
23

3-
from parse import Parser
4+
import re
5+
from dataclasses import dataclass
46

57

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

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

1314

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

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

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

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

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)