Repository: marshmallow-code/apispec

Issue:
Allow map_to_openapi_type to be called like a function
Hi,

so I have a schema which uses a custom field. Currently you expect that we do the following:

```
ma = MarshmallowPlugin()

@ma.map_to_openapi_type(fields.Dict)
class Custom(field.Field): pass
```

However, it's not very nice if the fields are in separate files since you always need the `MarshmallowPlugin` instance. As a workaround, I figured out, that the decorator doesn't really change something in the class, therefore I can also write:
`ma.map_to_openapi_type(fields.Dict)(Custom)`. However, still not very nice syntax.

Therefore I would like to propose a new function which just takes the two args: `ma.map_to_openapi_type2(Custom, fileds,Dict)` (name can be improved 😄 ). This can then be called whereever the `MarshmallowPlugin` is created. 
What do you think?

Comments:

Patch:
diff --git a/docs/using_plugins.rst b/docs/using_plugins.rst
index 0f02be9..8dd2b4b 100644
--- a/docs/using_plugins.rst
+++ b/docs/using_plugins.rst
@@ -243,7 +243,7 @@ custom field subclasses a standard marshmallow `Field` class then it will
 inherit the default mapping. If you want to override the OpenAPI type and format
 for custom fields, use the
 `map_to_openapi_type <apispec.ext.marshmallow.MarshmallowPlugin.map_to_openapi_type>`
-decorator. It can be invoked with either a pair of strings providing the
+method. It can be invoked with either a pair of strings providing the
 OpenAPI type and format, or a marshmallow `Field` that has the desired target mapping.
 
 .. code-block:: python
@@ -258,21 +258,26 @@ OpenAPI type and format, or a marshmallow `Field` that has the desired target ma
         title="Demo", version="0.1", openapi_version="3.0.0", plugins=(ma_plugin,)
     )
 
-    # Inherits Integer mapping of ('integer', 'int32')
-    class MyCustomInteger(Integer):
+    # Inherits Integer mapping of ('integer', None)
+    class CustomInteger(Integer):
         pass
 
 
     # Override Integer mapping
-    @ma_plugin.map_to_openapi_type("string", "uuid")
-    class MyCustomField(Integer):
+    class Int32(Integer):
         pass
 
 
-    @ma_plugin.map_to_openapi_type(Integer)  # will map to ('integer', 'int32')
-    class MyCustomFieldThatsKindaLikeAnInteger(Field):
+    ma_plugin.map_to_openapi_type(Int32, "string", "int32")
+
+
+    # Map to ('integer', None) like Integer
+    class IntegerLike(Field):
         pass
 
+
+    ma_plugin.map_to_openapi_type(IntegerLike, Integer)
+
 In situations where greater control of the properties generated for a custom field
 is desired, users may add custom logic to the conversion of fields to OpenAPI properties
 through the use of the `add_attribute_function
diff --git a/src/apispec/ext/marshmallow/__init__.py b/src/apispec/ext/marshmallow/__init__.py
index c08cb87..2595581 100644
--- a/src/apispec/ext/marshmallow/__init__.py
+++ b/src/apispec/ext/marshmallow/__init__.py
@@ -137,8 +137,10 @@ class MarshmallowPlugin(BasePlugin):
             openapi_version=spec.openapi_version, converter=self.converter
         )
 
-    def map_to_openapi_type(self, *args):
-        """Decorator to set mapping for custom fields.
+    def map_to_openapi_type(self, field_cls, *args):
+        """Set mapping for custom field class.
+
+        :param type field_cls: Field class to set mapping for.
 
         ``*args`` can be:
 
@@ -147,15 +149,19 @@ class MarshmallowPlugin(BasePlugin):
 
         Examples: ::
 
-            @ma_plugin.map_to_openapi_type('string', 'uuid')
-            class MyCustomField(Integer):
+            # Override Integer mapping
+            class Int32(Integer):
                 # ...
 
-            @ma_plugin.map_to_openapi_type(Integer)  # will map to ('integer', None)
-            class MyCustomFieldThatsKindaLikeAnInteger(Integer):
+            ma_plugin.map_to_openapi_type(Int32, 'string', 'int32')
+
+            # Map to ('integer', None) like Integer
+            class IntegerLike(Integer):
                 # ...
+
+            ma_plugin.map_to_openapi_type(IntegerLike, Integer)
         """
-        return self.converter.map_to_openapi_type(*args)
+        return self.converter.map_to_openapi_type(field_cls, *args)
 
     def schema_helper(self, name, _, schema=None, **kwargs):
         """Definition helper that allows using a marshmallow
diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py
index 2a5beaa..5c9abc5 100644
--- a/src/apispec/ext/marshmallow/field_converter.py
+++ b/src/apispec/ext/marshmallow/field_converter.py
@@ -113,8 +113,10 @@ class FieldConverterMixin:
             self.timedelta2properties,
         ]
 
-    def map_to_openapi_type(self, *args):
-        """Decorator to set mapping for custom fields.
+    def map_to_openapi_type(self, field_cls, *args):
+        """Set mapping for custom field class.
+
+        :param type field_cls: Field class to set mapping for.
 
         ``*args`` can be:
 
@@ -128,11 +130,7 @@ class FieldConverterMixin:
         else:
             raise TypeError("Pass core marshmallow field type or (type, fmt) pair.")
 
-        def inner(field_type):
-            self.field_mapping[field_type] = openapi_type_field
-            return field_type
-
-        return inner
+        self.field_mapping[field_cls] = openapi_type_field
 
     def add_attribute_function(self, func):
         """Method to add an attribute function to the list of attribute functions


Test patch:
diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py
index 5e8a61c..07d83fa 100644
--- a/tests/test_ext_marshmallow.py
+++ b/tests/test_ext_marshmallow.py
@@ -314,19 +314,25 @@ class TestComponentHeaderHelper:
 
 class TestCustomField:
     def test_can_use_custom_field_decorator(self, spec_fixture):
-        @spec_fixture.marshmallow_plugin.map_to_openapi_type(DateTime)
         class CustomNameA(Field):
             pass
 
-        @spec_fixture.marshmallow_plugin.map_to_openapi_type("integer", "int32")
+        spec_fixture.marshmallow_plugin.map_to_openapi_type(CustomNameA, DateTime)
+
         class CustomNameB(Field):
             pass
 
-        with pytest.raises(TypeError):
+        spec_fixture.marshmallow_plugin.map_to_openapi_type(
+            CustomNameB, "integer", "int32"
+        )
 
-            @spec_fixture.marshmallow_plugin.map_to_openapi_type("integer")
-            class BadCustomField(Field):
-                pass
+        class BadCustomField(Field):
+            pass
+
+        with pytest.raises(TypeError):
+            spec_fixture.marshmallow_plugin.map_to_openapi_type(
+                BadCustomField, "integer"
+            )
 
         class CustomPetASchema(PetSchema):
             name = CustomNameA()
diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py
index aab4304..dcba184 100644
--- a/tests/test_ext_marshmallow_field.py
+++ b/tests/test_ext_marshmallow_field.py
@@ -212,10 +212,11 @@ def test_field_with_range_string_type(spec_fixture, field):
 
 @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True)
 def test_field_with_range_type_list_with_number(spec_fixture):
-    @spec_fixture.openapi.map_to_openapi_type(["integer", "null"], None)
     class NullableInteger(fields.Field):
         """Nullable integer"""
 
+    spec_fixture.openapi.map_to_openapi_type(NullableInteger, ["integer", "null"], None)
+
     field = NullableInteger(validate=validate.Range(min=1, max=10))
     res = spec_fixture.openapi.field2property(field)
     assert res["minimum"] == 1
@@ -225,10 +226,11 @@ def test_field_with_range_type_list_with_number(spec_fixture):
 
 @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True)
 def test_field_with_range_type_list_without_number(spec_fixture):
-    @spec_fixture.openapi.map_to_openapi_type(["string", "null"], None)
     class NullableInteger(fields.Field):
         """Nullable integer"""
 
+    spec_fixture.openapi.map_to_openapi_type(NullableInteger, ["string", "null"], None)
+
     field = NullableInteger(validate=validate.Range(min=1, max=10))
     res = spec_fixture.openapi.field2property(field)
     assert res["x-minimum"] == 1