Repository: marshmallow-code/apispec
Pull request #464 (2019-06-24 15:29:11)

Deepcopy operations and paramers in path
https://github.com/marshmallow-code/apispec/issues/463#issuecomment-505030357


Patch:
diff --git a/src/apispec/core.py b/src/apispec/core.py
index 0ba1f5e..d6c5d70 100644
--- a/src/apispec/core.py
+++ b/src/apispec/core.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 """Core apispec classes and functions."""
 from collections import OrderedDict
+from copy import deepcopy
 import warnings
 
 from apispec.compat import iterkeys, iteritems
@@ -334,8 +335,10 @@ class APISpec(object):
         :param list|None parameters: list of parameters relevant to all operations in this path
         :param dict kwargs: parameters used by any path helpers see :meth:`register_path_helper`
         """
-        operations = operations or OrderedDict()
-        parameters = parameters or []
+        # operations and parameters must be deepcopied because they are mutated
+        # in clean_operations and operation helpers and path may be called twice
+        operations = deepcopy(operations) or OrderedDict()
+        parameters = deepcopy(parameters) or []
 
         # Execute path helpers
         for plugin in self.plugins:


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 5ad5dbf..03f66ee 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -360,6 +360,51 @@ class TestPath:
         assert "get" in p
         assert "put" in p
 
+    @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
+    def test_path_called_twice_with_same_operations_parameters(self, openapi_version):
+        """Test calling path twice with same operations or parameters
+
+        operations and parameters being mutated by clean_operations and plugin helpers
+        should not make path fail on second call
+        """
+
+        class TestPlugin(BasePlugin):
+            def path_helper(self, path, operations, parameters, **kwargs):
+                """Mutate operations and parameters"""
+                operations.update({"post": {"responses": {"201": "201ResponseRef"}}})
+                parameters.append("ParamRef_3")
+                return path
+
+        spec = APISpec(
+            title="Swagger Petstore",
+            version="1.0.0",
+            openapi_version=openapi_version,
+            plugins=[TestPlugin()],
+        )
+
+        path = "/pet/{petId}"
+        parameters = ["ParamRef_1"]
+        operation = {
+            "parameters": ["ParamRef_2"],
+            "responses": {"200": "200ResponseRef"},
+        }
+
+        spec.path(path=path, operations={"get": operation}, parameters=parameters)
+        spec.path(path=path, operations={"put": operation}, parameters=parameters)
+        operations = (get_paths(spec))[path]
+        assert (
+            operations["get"]
+            == operations["put"]
+            == {
+                "parameters": [build_ref(spec, "parameter", "ParamRef_2")],
+                "responses": {"200": build_ref(spec, "response", "200ResponseRef")},
+            }
+        )
+        assert operations["parameters"] == [
+            build_ref(spec, "parameter", "ParamRef_1"),
+            build_ref(spec, "parameter", "ParamRef_3"),
+        ]
+
     def test_path_ensures_path_parameters_required(self, spec):
         path = "/pet/{petId}"
         spec.path(