Skip to content

Commit

Permalink
Don't accept arbitrary arguments to auto_field (#647)
Browse files Browse the repository at this point in the history
* Don't accept arbitrary arguments to auto_field

* Update changelog
  • Loading branch information
sloria authored Jan 12, 2025
1 parent 649a7cf commit e27929e
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 38 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
Changelog
---------

1.4.0 (unreleased)
++++++++++++++++++

Other changes:

* Passing arbitrary keyword arguments to `auto_field <marshmallow_sqlalchemy.auto_field>`
is no longer supported (:pr:`647`). Use the ``metadata`` argument to pass metadata
to the generated field instead.

.. code-block:: python
# Before
auto_field(description="The name of the artist")
# On marshmallow 3, this raises a warning: "RemovedInMarshmallow4Warning: Passing field metadata as keyword arguments is deprecated."
# On marshmallow 4, this raises an error: "TypeError: Field.__init__() got an unexpected keyword argument 'description'"
# After
auto_field(metadata=dict(description="The name of the artist"))
1.3.0 (2025-01-11)
++++++++++++++++++

Expand Down
42 changes: 4 additions & 38 deletions src/marshmallow_sqlalchemy/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,33 +62,6 @@ def _enum_field_factory(
return fields.Enum if data_type.enum_class else fields.Raw


def _field_update_kwargs(
field_class: type[fields.Field] | functools.partial,
field_kwargs: dict[str, Any],
kwargs: dict[str, Any],
) -> dict[str, Any]:
if not kwargs:
return field_kwargs

if isinstance(field_class, functools.partial):
# Unwrap partials, assuming that they bind a Field to arguments
field_class = cast(functools.partial, field_class.func)

possible_field_keywords = {
key
for cls in inspect.getmro(cast(type[fields.Field], field_class))
for key, param in inspect.signature(cls.__init__).parameters.items() # type: ignore[misc]
if param.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
or param.kind is inspect.Parameter.KEYWORD_ONLY
}
for k, v in kwargs.items():
if k in possible_field_keywords:
field_kwargs[k] = v
else:
field_kwargs["metadata"][k] = v
return field_kwargs


class ModelConverter:
"""Converts a SQLAlchemy model into a dictionary of corresponding
marshmallow `Fields <marshmallow.fields.Field>`.
Expand Down Expand Up @@ -252,17 +225,14 @@ def property2field(
if not instance:
return field_class
field_kwargs = self._get_field_kwargs_for_property(prop)
_field_update_kwargs(field_class, field_kwargs, kwargs)
field_kwargs.update(kwargs)
ret = field_class(**field_kwargs)
if (
hasattr(prop, "direction")
and self.DIRECTION_MAPPING[prop.direction.name]
and prop.uselist is True
):
related_list_kwargs = _field_update_kwargs(
RelatedList, self.get_base_kwargs(), kwargs
)
ret = RelatedList(ret, **related_list_kwargs)
ret = RelatedList(ret, **{**self.get_base_kwargs(), **kwargs})
return ret

@overload
Expand Down Expand Up @@ -290,8 +260,7 @@ def column2field(
return field_class
field_kwargs = self.get_base_kwargs()
self._add_column_kwargs(field_kwargs, column)
_field_update_kwargs(field_class, field_kwargs, kwargs)
return field_class(**field_kwargs)
return field_class(**{**field_kwargs, **kwargs})

@overload
def field_for(
Expand Down Expand Up @@ -352,10 +321,7 @@ def field_for(
**kwargs,
)
if remote_with_local_multiplicity:
related_list_kwargs = _field_update_kwargs(
RelatedList, self.get_base_kwargs(), kwargs
)
return RelatedList(converted_prop, **related_list_kwargs)
return RelatedList(converted_prop, **{**self.get_base_kwargs(), **kwargs})
else:
return converted_prop

Expand Down
26 changes: 26 additions & 0 deletions tests/test_sqlalchemy_schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from importlib.metadata import version

import marshmallow
import pytest
import sqlalchemy as sa
Expand Down Expand Up @@ -672,3 +674,27 @@ class Meta(TeacherSchema.Meta):
assert "id" in schema2.fields
assert "inherited_field" in schema2.fields
assert "current_school_id" in schema2.fields


def test_auto_field_does_not_accept_arbitrary_kwargs(models):
if int(version("marshmallow")[0]) < 4:
from marshmallow.warnings import RemovedInMarshmallow4Warning

with pytest.warns(
RemovedInMarshmallow4Warning,
match="Passing field metadata as keyword arguments is deprecated",
):

class CourseSchema(SQLAlchemyAutoSchema):
class Meta:
model = models.Course

name = auto_field(description="A course name")
else:
with pytest.raises(TypeError, match="unexpected keyword argument"):

class CourseSchema(SQLAlchemyAutoSchema): # type: ignore[no-redef]
class Meta:
model = models.Course

name = auto_field(description="A course name")

0 comments on commit e27929e

Please sign in to comment.