Skip to content

Commit 41bdfa0

Browse files
committed
Ensure plugin helpers get component copies
1 parent 5932e61 commit 41bdfa0

File tree

2 files changed

+34
-18
lines changed

2 files changed

+34
-18
lines changed

src/apispec/core.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,11 @@ def schema(self, component_id, component=None, *, lazy=False, **kwargs):
125125
raise DuplicateComponentNameError(
126126
f'Another schema with name "{component_id}" is already registered.'
127127
)
128-
component = component or {}
129-
ret = component.copy()
128+
ret = deepcopy(component) or {}
130129
# Execute all helpers from plugins
131130
for plugin in self._plugins:
132131
try:
133-
ret.update(
134-
plugin.schema_helper(component_id, component, **kwargs) or {}
135-
)
132+
ret.update(plugin.schema_helper(component_id, ret, **kwargs) or {})
136133
except PluginMethodNotImplementedError:
137134
continue
138135
self._resolve_refs_in_schema(ret)
@@ -151,12 +148,11 @@ def response(self, component_id, component=None, *, lazy=False, **kwargs):
151148
raise DuplicateComponentNameError(
152149
f'Another response with name "{component_id}" is already registered.'
153150
)
154-
component = component or {}
155-
ret = component.copy()
151+
ret = deepcopy(component) or {}
156152
# Execute all helpers from plugins
157153
for plugin in self._plugins:
158154
try:
159-
ret.update(plugin.response_helper(component, **kwargs) or {})
155+
ret.update(plugin.response_helper(ret, **kwargs) or {})
160156
except PluginMethodNotImplementedError:
161157
continue
162158
self._resolve_refs_in_response(ret)
@@ -178,8 +174,7 @@ def parameter(
178174
raise DuplicateComponentNameError(
179175
f'Another parameter with name "{component_id}" is already registered.'
180176
)
181-
component = component or {}
182-
ret = component.copy()
177+
ret = deepcopy(component) or {}
183178
ret.setdefault("name", component_id)
184179
ret["in"] = location
185180

@@ -190,7 +185,7 @@ def parameter(
190185
# Execute all helpers from plugins
191186
for plugin in self._plugins:
192187
try:
193-
ret.update(plugin.parameter_helper(component, **kwargs) or {})
188+
ret.update(plugin.parameter_helper(ret, **kwargs) or {})
194189
except PluginMethodNotImplementedError:
195190
continue
196191
self._resolve_refs_in_parameter_or_header(ret)
@@ -207,16 +202,15 @@ def header(self, component_id, component, *, lazy=False, **kwargs):
207202
208203
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#headerObject
209204
"""
210-
component = component or {}
211-
ret = component.copy()
205+
ret = deepcopy(component) or {}
212206
if component_id in self.headers:
213207
raise DuplicateComponentNameError(
214208
f'Another header with name "{component_id}" is already registered.'
215209
)
216210
# Execute all helpers from plugins
217211
for plugin in self._plugins:
218212
try:
219-
ret.update(plugin.header_helper(component, **kwargs) or {})
213+
ret.update(plugin.header_helper(ret, **kwargs) or {})
220214
except PluginMethodNotImplementedError:
221215
continue
222216
self._resolve_refs_in_parameter_or_header(ret)

tests/test_core.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,19 +1028,29 @@ class TestPlugins:
10281028
@staticmethod
10291029
def test_plugin_factory(return_none=False):
10301030
class TestPlugin(BasePlugin):
1031+
"""Test Plugin
1032+
1033+
return_none allows to check plugin helpers returning ``None``
1034+
Inputs are mutated to allow testing only a copy is passed.
1035+
"""
1036+
10311037
def schema_helper(self, name, definition, **kwargs):
1038+
definition.pop("dummy", None)
10321039
if not return_none:
10331040
return {"properties": {"name": {"type": "string"}}}
10341041

10351042
def parameter_helper(self, parameter, **kwargs):
1043+
parameter.pop("dummy", None)
10361044
if not return_none:
10371045
return {"description": "some parameter"}
10381046

10391047
def response_helper(self, response, **kwargs):
1048+
response.pop("dummy", None)
10401049
if not return_none:
10411050
return {"description": "42"}
10421051

10431052
def header_helper(self, header, **kwargs):
1053+
header.pop("dummy", None)
10441054
if not return_none:
10451055
return {"description": "some header"}
10461056

@@ -1066,12 +1076,15 @@ def test_plugin_schema_helper_is_used(self, openapi_version, return_none):
10661076
openapi_version=openapi_version,
10671077
plugins=(self.test_plugin_factory(return_none),),
10681078
)
1069-
spec.components.schema("Pet")
1079+
schema = {"dummy": "dummy"}
1080+
spec.components.schema("Pet", schema)
10701081
definitions = get_schemas(spec)
10711082
if return_none:
10721083
assert definitions["Pet"] == {}
10731084
else:
10741085
assert definitions["Pet"] == {"properties": {"name": {"type": "string"}}}
1086+
# Check original schema is not modified
1087+
assert schema == {"dummy": "dummy"}
10751088

10761089
@pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
10771090
@pytest.mark.parametrize("return_none", (True, False))
@@ -1082,7 +1095,8 @@ def test_plugin_parameter_helper_is_used(self, openapi_version, return_none):
10821095
openapi_version=openapi_version,
10831096
plugins=(self.test_plugin_factory(return_none),),
10841097
)
1085-
spec.components.parameter("Pet", "body", {})
1098+
parameter = {"dummy": "dummy"}
1099+
spec.components.parameter("Pet", "body", parameter)
10861100
parameters = get_parameters(spec)
10871101
if return_none:
10881102
assert parameters["Pet"] == {"in": "body", "name": "Pet"}
@@ -1092,6 +1106,8 @@ def test_plugin_parameter_helper_is_used(self, openapi_version, return_none):
10921106
"name": "Pet",
10931107
"description": "some parameter",
10941108
}
1109+
# Check original parameter is not modified
1110+
assert parameter == {"dummy": "dummy"}
10951111

10961112
@pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
10971113
@pytest.mark.parametrize("return_none", (True, False))
@@ -1102,12 +1118,15 @@ def test_plugin_response_helper_is_used(self, openapi_version, return_none):
11021118
openapi_version=openapi_version,
11031119
plugins=(self.test_plugin_factory(return_none),),
11041120
)
1105-
spec.components.response("Pet", {})
1121+
response = {"dummy": "dummy"}
1122+
spec.components.response("Pet", response)
11061123
responses = get_responses(spec)
11071124
if return_none:
11081125
assert responses["Pet"] == {}
11091126
else:
11101127
assert responses["Pet"] == {"description": "42"}
1128+
# Check original response is not modified
1129+
assert response == {"dummy": "dummy"}
11111130

11121131
@pytest.mark.parametrize("openapi_version", ("3.0.0",))
11131132
@pytest.mark.parametrize("return_none", (True, False))
@@ -1118,14 +1137,17 @@ def test_plugin_header_helper_is_used(self, openapi_version, return_none):
11181137
openapi_version=openapi_version,
11191138
plugins=(self.test_plugin_factory(return_none),),
11201139
)
1121-
spec.components.header("Pet", {})
1140+
header = {"dummy": "dummy"}
1141+
spec.components.header("Pet", header)
11221142
headers = get_headers(spec)
11231143
if return_none:
11241144
assert headers["Pet"] == {}
11251145
else:
11261146
assert headers["Pet"] == {
11271147
"description": "some header",
11281148
}
1149+
# Check original header is not modified
1150+
assert header == {"dummy": "dummy"}
11291151

11301152
@pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
11311153
@pytest.mark.parametrize("return_none", (True, False))

0 commit comments

Comments
 (0)