diff --git a/CHANGELOG.rst b/CHANGELOG.rst index edc9a25..94332ef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,41 @@ Changelog ========= +0.14.0 (unreleased) +******************* + +* Add ``values`` argument to ``URLFor`` and ``AbsoluteURLFor`` for passing values to ``flask.url_for``. + This prevents unrelated parameters from getting passed (:issue:`52`, :issue:`67`). + Thanks :user:`AlrasheedA` for the PR. + +Deprecation: + +* Passing params to ``flask.url_for`` via ``URLFor``'s and ``AbsoluteURLFor``'s constructor + params is deprecated. Pass ``values`` instead. + +.. code-block:: python + + # flask-marshmallow<0.14.0 + + + class UserSchema(ma.Schema): + _links = ma.Hyperlinks( + { + "self": ma.URLFor("user_detail", id=""), + } + ) + + + # flask-marshmallow>=0.14.0 + + + class UserSchema(ma.Schema): + _links = ma.Hyperlinks( + { + "self": ma.URLFor("user_detail", values=dict(id="")), + } + ) + 0.13.0 (2020-06-07) ******************* @@ -20,8 +55,8 @@ Other changes: .. warning:: It is highly recommended that you use the newer ``ma.SQLAlchemySchema`` and ``ma.SQLAlchemyAutoSchema`` classes - instead of ``ModelSchema`` and ``TableSchema``. See the release notes for `marshmallow-sqlalchemy 0.22.0 `_ - for instructions on how to migrate. + instead of ``ModelSchema`` and ``TableSchema``. See the release notes for `marshmallow-sqlalchemy 0.22.0 `_ + for instructions on how to migrate. If you need to use ``ModelSchema`` and ``TableSchema`` for the time being, you'll need to import these directly from ``marshmallow_sqlalchemy``. diff --git a/README.rst b/README.rst index e2fcf58..44ffc71 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,10 @@ Define your output format with marshmallow. # Smart hyperlinking _links = ma.Hyperlinks( - {"self": ma.URLFor("user_detail", id=""), "collection": ma.URLFor("users")} + { + "self": ma.URLFor("user_detail", values=dict(id="")), + "collection": ma.URLFor("users"), + } ) diff --git a/docs/index.rst b/docs/index.rst index d5d499b..b803607 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,7 +55,10 @@ Define your output format with marshmallow. # Smart hyperlinking _links = ma.Hyperlinks( - {"self": ma.URLFor("user_detail", id=""), "collection": ma.URLFor("users")} + { + "self": ma.URLFor("user_detail", values=dict(id="")), + "collection": ma.URLFor("users"), + } ) diff --git a/src/flask_marshmallow/__init__.py b/src/flask_marshmallow/__init__.py index 8ed1bda..378cf8c 100755 --- a/src/flask_marshmallow/__init__.py +++ b/src/flask_marshmallow/__init__.py @@ -71,7 +71,7 @@ class Meta: author = ma.Nested(AuthorSchema) links = ma.Hyperlinks({ - 'self': ma.URLFor('book_detail', id=''), + 'self': ma.URLFor('book_detail', values=dict(id='')), 'collection': ma.URLFor('book_list') }) diff --git a/src/flask_marshmallow/fields.py b/src/flask_marshmallow/fields.py index d2c0f9c..b8c75a4 100755 --- a/src/flask_marshmallow/fields.py +++ b/src/flask_marshmallow/fields.py @@ -64,24 +64,26 @@ def _get_value_for_key(obj, key, default): class URLFor(fields.Field): """Field that outputs the URL for an endpoint. Acts identically to Flask's ``url_for`` function, except that arguments can be pulled from the - object to be serialized. + object to be serialized, and ``**values`` should be passed to the ``values`` + parameter. Usage: :: - url = URLFor('author_get', id='') - https_url = URLFor('author_get', id='', _scheme='https', _external=True) + url = URLFor('author_get', values=dict(id='')) + https_url = URLFor('author_get', values=dict(id='', _scheme='https', _external=True)) :param str endpoint: Flask endpoint name. - :param kwargs: Same keyword arguments as Flask's url_for, except string + :param dict values: Same keyword arguments as Flask's url_for, except string arguments enclosed in `< >` will be interpreted as attributes to pull from the object. + :param kwargs: keyword arguments to pass to marshmallow field (e.g. ``required``). """ _CHECK_ATTRIBUTE = False - def __init__(self, endpoint, **kwargs): + def __init__(self, endpoint, values=None, **kwargs): self.endpoint = endpoint - self.params = kwargs + self.values = values or kwargs # kwargs for backward compatibility fields.Field.__init__(self, **kwargs) def _serialize(self, value, key, obj): @@ -89,7 +91,7 @@ def _serialize(self, value, key, obj): ``__init__``. """ param_values = {} - for name, attr_tpl in self.params.items(): + for name, attr_tpl in self.values.items(): attr_name = _tpl(str(attr_tpl)) if attr_name: attribute_value = _get_value(obj, attr_name, default=missing) @@ -113,9 +115,12 @@ def _serialize(self, value, key, obj): class AbsoluteURLFor(URLFor): """Field that outputs the absolute URL for an endpoint.""" - def __init__(self, endpoint, **kwargs): - kwargs["_external"] = True - URLFor.__init__(self, endpoint=endpoint, **kwargs) + def __init__(self, endpoint, values=None, **kwargs): + if values: # for backward compatibility + values["_external"] = True + else: + kwargs["_external"] = True + URLFor.__init__(self, endpoint=endpoint, values=values, **kwargs) AbsoluteUrlFor = AbsoluteURLFor @@ -149,7 +154,7 @@ class Hyperlinks(fields.Field): Example: :: _links = Hyperlinks({ - 'self': URLFor('author', id=''), + 'self': URLFor('author', values=dict(id='')), 'collection': URLFor('author_list'), }) @@ -157,7 +162,7 @@ class Hyperlinks(fields.Field): _links = Hyperlinks({ 'self': { - 'href': URLFor('book', id=''), + 'href': URLFor('book', values=dict(id='')), 'title': 'book detail' } }) diff --git a/tests/test_fields.py b/tests/test_fields.py index 4f9283d..585e859 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -19,6 +19,10 @@ def test_url_field(ma, mockauthor): result = field.serialize("url", mockauthor) assert result == url_for("author", id=mockauthor.id) + field = ma.URLFor("author", values=dict(id="")) + result = field.serialize("url", mockauthor) + assert result == url_for("author", id=mockauthor.id) + mockauthor.id = 0 result = field.serialize("url", mockauthor) assert result == url_for("author", id=0) @@ -32,12 +36,23 @@ def test_url_field_with_invalid_attribute(ma, mockauthor): with pytest.raises(AttributeError, match=expected_msg): field.serialize("url", mockauthor) + field = ma.URLFor("author", values=dict(id="")) + expected_msg = "{!r} is not a valid attribute of {!r}".format( + "not-an-attr", mockauthor + ) + with pytest.raises(AttributeError, match=expected_msg): + field.serialize("url", mockauthor) + def test_url_field_handles_nested_attribute(ma, mockbook, mockauthor): field = ma.URLFor("author", id="") result = field.serialize("url", mockbook) assert result == url_for("author", id=mockauthor.id) + field = ma.URLFor("author", values=dict(id="")) + result = field.serialize("url", mockbook) + assert result == url_for("author", id=mockauthor.id) + def test_url_field_handles_none_attribute(ma, mockbook, mockauthor): mockbook.author = None @@ -50,6 +65,14 @@ def test_url_field_handles_none_attribute(ma, mockbook, mockauthor): result = field.serialize("url", mockbook) assert result is None + field = ma.URLFor("author", values=dict(id="")) + result = field.serialize("url", mockbook) + assert result is None + + field = ma.URLFor("author", values=dict(id="")) + result = field.serialize("url", mockbook) + assert result is None + def test_url_field_deserialization(ma): field = ma.URLFor("author", id="", allow_none=True) @@ -57,6 +80,11 @@ def test_url_field_deserialization(ma): assert field.deserialize("foo") == "foo" assert field.deserialize(None) is None + field = ma.URLFor("author", values=dict(id=""), allow_none=True) + # noop + assert field.deserialize("foo") == "foo" + assert field.deserialize(None) is None + def test_invalid_endpoint_raises_build_error(ma, mockauthor): field = ma.URLFor("badendpoint") diff --git a/tox.ini b/tox.ini index 16b80ed..48255c3 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ commands = sphinx-build docs/ docs/_build {posargs} deps = sphinx-autobuild extras = docs -commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/flask_marshmallow -s 2 +commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/flask_marshmallow --delay 2 [testenv:watch-readme] deps = restview