Skip to content

Commit 3cb7cd9

Browse files
committed
Fix PATH_PARAMETER_PATTERN for DRF default value pattern.
Django REST Framework will use `[^/.]+` as the default value pattern in certain cases, e.g. `(?P<pk>[^/.]+)`, which doesn't work well with the current regular expression in `PATH_PARAMETER_PATTERN`. ``` ❯ git describe; git rev-parse HEAD 3.14.0-69-g0618fa88 0618fa88e1a8c2cf8a2aab29ef6de66b49e5f7ed ❯ git grep -n '\[^/.\]+' rest_framework/routers.py:143: self._default_value_pattern = '[^/.]+' tests/test_routers.py:304: expected = ['^notes/$', '^notes/(?P<pk>[^/.]+)/$'] tests/test_routers.py:319: expected = ['^notes$', '^notes/(?P<pk>[^/.]+)$'] ``` The fix inserts `(?:[^/]*?\[\^[^/]*/)?` before the final `[^/]*` in the pattern. This allows matching a slash inside an inverted character set, i.e. `[^...]`, treating it as part of the named group.
1 parent eefb1d6 commit 3cb7cd9

File tree

2 files changed

+49
-60
lines changed

2 files changed

+49
-60
lines changed

openapi_core/contrib/django/requests.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@
88

99
from openapi_core.validation.request.datatypes import RequestParameters
1010

11-
# https://docs.djangoproject.com/en/2.2/topics/http/urls/
11+
# https://docs.djangoproject.com/en/stable/topics/http/urls/
1212
#
1313
# Currently unsupported are :
1414
# - nested arguments, e.g.: ^comments/(?:page-(?P<page_number>\d+)/)?$
1515
# - unnamed regex groups, e.g.: ^articles/([0-9]{4})/$
1616
# - multiple named parameters between a single pair of slashes
1717
# e.g.: <page_slug>-<page_id>/edit/
1818
#
19-
# The regex matches everything, except a "/" until "<". Than only the name
19+
# The regex matches everything, except a "/" until "<". Then only the name
2020
# is exported, after which it matches ">" and everything until a "/".
21-
PATH_PARAMETER_PATTERN = r"(?:[^\/]*?)<(?:(?:.*?:))*?(\w+)>(?:[^\/]*)"
21+
# A check is made to ensure that "/" is not in an excluded character set such
22+
# as may be found with Django REST Framwork's default value pattern, "[^/.]+".
23+
PATH_PARAMETER_PATTERN = r"(?:[^/]*?)<(?:(?:.*?:))*?(\w+)>(?:(?:[^/]*?\[\^[^/]*/)?[^/]*)"
2224

2325

2426
class DjangoOpenAPIRequest:

tests/unit/contrib/django/test_django.py

+44-57
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def django_settings(self):
4343
settings.ROOT_URLCONF = (
4444
path("admin/", admin.site.urls),
4545
re_path("^test/test-regexp/$", lambda d: None),
46+
re_path("^object/(?P<pk>[^/.]+)/action/$", lambda d: None),
4647
)
4748

4849
@pytest.fixture
@@ -68,81 +69,53 @@ def test_no_resolver(self, request_factory):
6869

6970
openapi_request = DjangoOpenAPIRequest(request)
7071

71-
path = {}
72-
query = ImmutableMultiDict(
73-
[
74-
("test1", "test2"),
75-
]
76-
)
77-
headers = Headers(
78-
{
79-
"Cookie": "",
80-
}
81-
)
82-
cookies = {}
8372
assert openapi_request.parameters == RequestParameters(
84-
path=path,
85-
query=query,
86-
header=headers,
87-
cookie=cookies,
73+
path={},
74+
query=ImmutableMultiDict([("test1", "test2")]),
75+
header=Headers({"Cookie": ""}),
76+
cookie={},
8877
)
8978
assert openapi_request.method == request.method.lower()
9079
assert openapi_request.host_url == request._current_scheme_host
9180
assert openapi_request.path == request.path
81+
assert openapi_request.path_pattern is None
9282
assert openapi_request.body == ""
9383
assert openapi_request.mimetype == request.content_type
9484

9585
def test_simple(self, request_factory):
9686
from django.urls import resolve
9787

9888
request = request_factory.get("/admin/")
99-
request.resolver_match = resolve("/admin/")
89+
request.resolver_match = resolve(request.path)
10090

10191
openapi_request = DjangoOpenAPIRequest(request)
10292

103-
path = {}
104-
query = {}
105-
headers = Headers(
106-
{
107-
"Cookie": "",
108-
}
109-
)
110-
cookies = {}
11193
assert openapi_request.parameters == RequestParameters(
112-
path=path,
113-
query=query,
114-
header=headers,
115-
cookie=cookies,
94+
path={},
95+
query={},
96+
header=Headers({"Cookie": ""}),
97+
cookie={},
11698
)
11799
assert openapi_request.method == request.method.lower()
118100
assert openapi_request.host_url == request._current_scheme_host
119101
assert openapi_request.path == request.path
102+
assert openapi_request.path_pattern == request.path
120103
assert openapi_request.body == ""
121104
assert openapi_request.mimetype == request.content_type
122105

123106
def test_url_rule(self, request_factory):
124107
from django.urls import resolve
125108

126109
request = request_factory.get("/admin/auth/group/1/")
127-
request.resolver_match = resolve("/admin/auth/group/1/")
110+
request.resolver_match = resolve(request.path)
128111

129112
openapi_request = DjangoOpenAPIRequest(request)
130113

131-
path = {
132-
"object_id": "1",
133-
}
134-
query = {}
135-
headers = Headers(
136-
{
137-
"Cookie": "",
138-
}
139-
)
140-
cookies = {}
141114
assert openapi_request.parameters == RequestParameters(
142-
path=path,
143-
query=query,
144-
header=headers,
145-
cookie=cookies,
115+
path={"object_id": "1"},
116+
query={},
117+
header=Headers({"Cookie": ""}),
118+
cookie={},
146119
)
147120
assert openapi_request.method == request.method.lower()
148121
assert openapi_request.host_url == request._current_scheme_host
@@ -155,27 +128,41 @@ def test_url_regexp_pattern(self, request_factory):
155128
from django.urls import resolve
156129

157130
request = request_factory.get("/test/test-regexp/")
158-
request.resolver_match = resolve("/test/test-regexp/")
131+
request.resolver_match = resolve(request.path)
159132

160133
openapi_request = DjangoOpenAPIRequest(request)
161134

162-
path = {}
163-
query = {}
164-
headers = Headers(
165-
{
166-
"Cookie": "",
167-
}
135+
assert openapi_request.parameters == RequestParameters(
136+
path={},
137+
query={},
138+
header=Headers({"Cookie": ""}),
139+
cookie={},
168140
)
169-
cookies = {}
141+
assert openapi_request.method == request.method.lower()
142+
assert openapi_request.host_url == request._current_scheme_host
143+
assert openapi_request.path == request.path
144+
assert openapi_request.path_pattern == request.path
145+
assert openapi_request.body == ""
146+
assert openapi_request.mimetype == request.content_type
147+
148+
def test_drf_default_value_pattern(self, request_factory):
149+
from django.urls import resolve
150+
151+
request = request_factory.get("/object/123/action/")
152+
request.resolver_match = resolve(request.path)
153+
154+
openapi_request = DjangoOpenAPIRequest(request)
155+
170156
assert openapi_request.parameters == RequestParameters(
171-
path=path,
172-
query=query,
173-
header=headers,
174-
cookie=cookies,
157+
path={"pk": "123"},
158+
query={},
159+
header=Headers({"Cookie": ""}),
160+
cookie={},
175161
)
176162
assert openapi_request.method == request.method.lower()
177163
assert openapi_request.host_url == request._current_scheme_host
178-
assert openapi_request.path == "/test/test-regexp/"
164+
assert openapi_request.path == request.path
165+
assert openapi_request.path_pattern == "/object/{pk}/action/"
179166
assert openapi_request.body == ""
180167
assert openapi_request.mimetype == request.content_type
181168

0 commit comments

Comments
 (0)