Repository: marshmallow-code/apispec
Pull request #414 (2019-03-29 23:28:03)

 Fix reference parameter/response with MarshmallowPlugin 
`MarshmallowPlugin` doesn't play well with parameter references because it assumes a parameter is a `dict`, which is not the case when passing a reference by name (the plugin works before `clean_operations` transforms the name into a full reference `dict`: `{"$ref": name})`, and it tries a `get` on the string.

This PR fixes this.

It also addresses a similar issue with responses. There was no error in this case because the resolve function does not use `get` but only `in`, which doesn't crash because the string is iterable, but I figured this was luck and it would be better to eliminate `dict` instances first.






Patch:
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 2558208..05f5d76 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,4 +1,4 @@
-marshmallow==2.19.2
+marshmallow==2.19.1
 pyyaml==5.1
 sphinx==2.0.0
 sphinx-issues==1.2.0
diff --git a/setup.py b/setup.py
index 1fe683b..a6982cd 100644
--- a/setup.py
+++ b/setup.py
@@ -9,13 +9,13 @@ EXTRAS_REQUIRE = {
     "lint": [
         "flake8==3.7.7",
         'flake8-bugbear==19.3.0; python_version >= "3.5"',
-        "pre-commit==1.15.0",
+        "pre-commit==1.14.4",
     ],
 }
 EXTRAS_REQUIRE["tests"] = (
     EXTRAS_REQUIRE["yaml"]
     + EXTRAS_REQUIRE["validation"]
-    + ["marshmallow==2.19.2", "pytest", "mock"]
+    + ["marshmallow==2.19.1", "pytest", "mock"]
 )
 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
 
diff --git a/src/apispec/ext/marshmallow/__init__.py b/src/apispec/ext/marshmallow/__init__.py
index 283dc19..d96537a 100644
--- a/src/apispec/ext/marshmallow/__init__.py
+++ b/src/apispec/ext/marshmallow/__init__.py
@@ -83,7 +83,9 @@ class MarshmallowPlugin(BasePlugin):
     def resolve_parameters(self, parameters):
         resolved = []
         for parameter in parameters:
-            if not isinstance(parameter.get("schema", {}), dict):
+            if isinstance(parameter, dict) and not isinstance(
+                parameter.get("schema", {}), dict
+            ):
                 schema_instance = resolve_schema_instance(parameter["schema"])
                 if "in" in parameter:
                     del parameter["schema"]
@@ -109,8 +111,12 @@ class MarshmallowPlugin(BasePlugin):
         corresponding dict to convert Marshmallow Schema object or class into dict
 
         :param APISpec spec: `APISpec` containing refs.
-        :param dict data: the parameter or response dictionary that may contain a schema
+        :param dict|str data: either a parameter or response dictionary that may
+            contain a schema, or a reference provided as string
         """
+        if not isinstance(data, dict):
+            return
+
         # OAS 2 component or OAS 3 header
         if "schema" in data:
             data["schema"] = self.openapi.resolve_schema_dict(data["schema"])


Test patch:
diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py
index 0b1b328..56dd0f1 100644
--- a/tests/test_ext_marshmallow.py
+++ b/tests/test_ext_marshmallow.py
@@ -571,7 +571,7 @@ class TestOperationHelper:
             },
         )
         get = get_paths(spec_fixture.spec)["/pet"]["get"]
-        len(get["parameters"]) == 1
+        assert len(get["parameters"]) == 1
         resolved_schema = {
             "type": "array",
             "items": {"$ref": ref_path(spec_fixture.spec) + "Pet"},
@@ -608,16 +608,15 @@ class TestOperationHelper:
                 }
             },
         )
-        p = get_paths(spec_fixture.spec)["/pet"]
-        assert "get" in p
-        op = p["get"]
+        get = get_paths(spec_fixture.spec)["/pet"]["get"]
+        assert len(get["parameters"]) == 1
         resolved_schema = {
             "type": "array",
             "items": {"$ref": ref_path(spec_fixture.spec) + "Pet"},
         }
-        request_schema = op["parameters"][0]["content"]["application/json"]["schema"]
+        request_schema = get["parameters"][0]["content"]["application/json"]["schema"]
         assert request_schema == resolved_schema
-        response_schema = op["responses"][200]["content"]["application/json"]["schema"]
+        response_schema = get["responses"][200]["content"]["application/json"]["schema"]
         assert response_schema == resolved_schema
 
     @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
@@ -684,6 +683,34 @@ class TestOperationHelper:
             },
         }
 
+    def test_parameter_reference(self, spec_fixture):
+        if spec_fixture.spec.openapi_version.major < 3:
+            param = {"schema": PetSchema}
+            reference = "#/parameters/Pet"
+        else:
+            param = {"content": {"application/json": {"schema": PetSchema}}}
+            reference = "#/components/parameters/Pet"
+        spec_fixture.spec.components.parameter("Pet", "body", param)
+        spec_fixture.spec.path(
+            path="/parents", operations={"get": {"parameters": ["Pet"]}}
+        )
+        get = get_paths(spec_fixture.spec)["/parents"]["get"]
+        assert get["parameters"] == [{"$ref": reference}]
+
+    def test_response_reference(self, spec_fixture):
+        if spec_fixture.spec.openapi_version.major < 3:
+            resp = {"schema": PetSchema}
+            reference = "#/responses/Pet"
+        else:
+            resp = {"content": {"application/json": {"schema": PetSchema}}}
+            reference = "#/components/responses/Pet"
+        spec_fixture.spec.components.response("Pet", resp)
+        spec_fixture.spec.path(
+            path="/parents", operations={"get": {"responses": {"200": "Pet"}}}
+        )
+        get = get_paths(spec_fixture.spec)["/parents"]["get"]
+        assert get["responses"] == {"200": {"$ref": reference}}
+
     def test_schema_global_state_untouched_2json(self, spec_fixture):
         assert RunSchema._declared_fields["sample"]._Nested__schema is None
         data = spec_fixture.openapi.schema2jsonschema(RunSchema)