Repository: marshmallow-code/apispec
Pull request #634 (2021-02-05 21:37:44)

Make components dicts public attributes of Components class
This allows the caller to check a component is already registered. For instance to avoid registering it several times.

I could try/except but I think exceptions are more suited for... exceptions. If testing is the normal flow (because the code is expected to run several time and only register once) I think the try/except method does not express the intent.

Another reason is that even in apispec itself, `_schemas` is accessed from another class.

flask-smorest auto registers default responses when a response contains a name from `http.HTTPStatus`. I'd like to allow the user to register his own response and only register a default response if he didn't. There my code needs to register the response only if it is not in components.schemas and it is a pity to keep track of which is registered in another list. More on this in https://github.com/marshmallow-code/flask-smorest/pull/208. This use case is not that important. I just don't see why those should be private.


Patch:
diff --git a/src/apispec/core.py b/src/apispec/core.py
index f46a40b..eab32b3 100644
--- a/src/apispec/core.py
+++ b/src/apispec/core.py
@@ -29,19 +29,19 @@ class Components:
     def __init__(self, plugins, openapi_version):
         self._plugins = plugins
         self.openapi_version = openapi_version
-        self._schemas = {}
-        self._responses = {}
-        self._parameters = {}
-        self._examples = {}
-        self._security_schemes = {}
+        self.schemas = {}
+        self.responses = {}
+        self.parameters = {}
+        self.examples = {}
+        self.security_schemes = {}
 
     def to_dict(self):
         subsections = {
-            "schema": self._schemas,
-            "response": self._responses,
-            "parameter": self._parameters,
-            "example": self._examples,
-            "security_scheme": self._security_schemes,
+            "schema": self.schemas,
+            "response": self.responses,
+            "parameter": self.parameters,
+            "example": self.examples,
+            "security_scheme": self.security_schemes,
         }
         return {
             COMPONENT_SUBSECTIONS[self.openapi_version.major][k]: v
@@ -70,7 +70,7 @@ class Components:
 
         https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
         """
-        if name in self._schemas:
+        if name in self.schemas:
             raise DuplicateComponentNameError(
                 f'Another schema with name "{name}" is already registered.'
             )
@@ -82,7 +82,7 @@ class Components:
                 ret.update(plugin.schema_helper(name, component, **kwargs) or {})
             except PluginMethodNotImplementedError:
                 continue
-        self._schemas[name] = ret
+        self.schemas[name] = ret
         return self
 
     def response(self, component_id, component=None, **kwargs):
@@ -92,7 +92,7 @@ class Components:
         :param dict component: response fields
         :param kwargs: plugin-specific arguments
         """
-        if component_id in self._responses:
+        if component_id in self.responses:
             raise DuplicateComponentNameError(
                 'Another response with name "{}" is already registered.'.format(
                     component_id
@@ -106,7 +106,7 @@ class Components:
                 ret.update(plugin.response_helper(component, **kwargs) or {})
             except PluginMethodNotImplementedError:
                 continue
-        self._responses[component_id] = ret
+        self.responses[component_id] = ret
         return self
 
     def parameter(self, component_id, location, component=None, **kwargs):
@@ -117,7 +117,7 @@ class Components:
         :param dict component: parameter fields.
         :param kwargs: plugin-specific arguments
         """
-        if component_id in self._parameters:
+        if component_id in self.parameters:
             raise DuplicateComponentNameError(
                 'Another parameter with name "{}" is already registered.'.format(
                     component_id
@@ -138,7 +138,7 @@ class Components:
                 ret.update(plugin.parameter_helper(component, **kwargs) or {})
             except PluginMethodNotImplementedError:
                 continue
-        self._parameters[component_id] = ret
+        self.parameters[component_id] = ret
         return self
 
     def example(self, name, component, **kwargs):
@@ -149,11 +149,11 @@ class Components:
 
         https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#exampleObject
         """
-        if name in self._examples:
+        if name in self.examples:
             raise DuplicateComponentNameError(
                 f'Another example with name "{name}" is already registered.'
             )
-        self._examples[name] = component
+        self.examples[name] = component
         return self
 
     def security_scheme(self, component_id, component):
@@ -162,13 +162,13 @@ class Components:
         :param str component_id: component_id to use as reference
         :param dict component: security scheme fields
         """
-        if component_id in self._security_schemes:
+        if component_id in self.security_schemes:
             raise DuplicateComponentNameError(
                 'Another security scheme with name "{}" is already registered.'.format(
                     component_id
                 )
             )
-        self._security_schemes[component_id] = component
+        self.security_schemes[component_id] = component
         return self
 
 
diff --git a/src/apispec/ext/marshmallow/common.py b/src/apispec/ext/marshmallow/common.py
index a781da5..9f773a8 100644
--- a/src/apispec/ext/marshmallow/common.py
+++ b/src/apispec/ext/marshmallow/common.py
@@ -123,7 +123,7 @@ def get_unique_schema_name(components, name, counter=0):
     :param int counter: the counter of the number of recursions
     :return: the unique name
     """
-    if name not in components._schemas:
+    if name not in components.schemas:
         return name
     if not counter:  # first time through recursion
         warnings.warn(


Test patch:
diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py
index d931065..9062efa 100644
--- a/tests/test_ext_marshmallow.py
+++ b/tests/test_ext_marshmallow.py
@@ -232,7 +232,7 @@ class TestComponentParameterHelper:
             reference = parameter["content"]["application/json"]["schema"]
         assert reference == build_ref(spec, "schema", "Pet")
 
-        resolved_schema = spec.components._schemas["Pet"]
+        resolved_schema = spec.components.schemas["Pet"]
         assert resolved_schema["properties"]["name"]["type"] == "string"
         assert resolved_schema["properties"]["password"]["type"] == "string"
         assert resolved_schema["properties"]["id"]["type"] == "integer"
@@ -253,7 +253,7 @@ class TestComponentResponseHelper:
             reference = response["content"]["application/json"]["schema"]
         assert reference == build_ref(spec, "schema", "Pet")
 
-        resolved_schema = spec.components._schemas["Pet"]
+        resolved_schema = spec.components.schemas["Pet"]
         assert resolved_schema["properties"]["id"]["type"] == "integer"
         assert resolved_schema["properties"]["name"]["type"] == "string"
         assert resolved_schema["properties"]["password"]["type"] == "string"
@@ -266,7 +266,7 @@ class TestComponentResponseHelper:
         reference = response["headers"]["PetHeader"]["schema"]
         assert reference == build_ref(spec, "schema", "Pet")
 
-        resolved_schema = spec.components._schemas["Pet"]
+        resolved_schema = spec.components.schemas["Pet"]
         assert resolved_schema["properties"]["id"]["type"] == "integer"
         assert resolved_schema["properties"]["name"]["type"] == "string"
         assert resolved_schema["properties"]["password"]["type"] == "string"
@@ -376,8 +376,8 @@ class TestOperationHelper:
             header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]
         assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
         assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
-        assert len(spec_fixture.spec.components._schemas) == 1
-        resolved_schema = spec_fixture.spec.components._schemas["Pet"]
+        assert len(spec_fixture.spec.components.schemas) == 1
+        resolved_schema = spec_fixture.spec.components.schemas["Pet"]
         assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
         assert get["responses"]["200"]["description"] == "successful operation"
 
@@ -425,8 +425,8 @@ class TestOperationHelper:
 
         assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
         assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
-        assert len(spec_fixture.spec.components._schemas) == 1
-        resolved_schema = spec_fixture.spec.components._schemas["Pet"]
+        assert len(spec_fixture.spec.components.schemas) == 1
+        resolved_schema = spec_fixture.spec.components.schemas["Pet"]
         assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
         assert get["responses"]["200"]["description"] == "successful operation"
 
@@ -475,8 +475,8 @@ class TestOperationHelper:
 
         assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
         assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
-        assert len(spec_fixture.spec.components._schemas) == 1
-        resolved_schema = spec_fixture.spec.components._schemas["Pet"]
+        assert len(spec_fixture.spec.components.schemas) == 1
+        resolved_schema = spec_fixture.spec.components.schemas["Pet"]
         assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
         assert get["responses"]["200"]["description"] == "successful operation"