Comments (7)
Are you sure that the difference is between your form_and_files
location and the files
location? When I test out your custom field and location, it seems to me that it's working correctly.
Here's a runnable example script
import io
import flask
import marshmallow as ma
from webargs.multidictproxy import MultiDictProxy
from webargs.flaskparser import use_args, parser
from werkzeug.datastructures import FileStorage
@parser.location_loader("form_and_files")
def load_form_and_files(request, schema):
form_and_files_data = request.files.copy()
form_and_files_data.update(request.form)
return MultiDictProxy(form_and_files_data, schema)
class FileField(ma.fields.Field):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.metadata["type"] = "string"
self.metadata["format"] = "binary"
default_error_messages = {"invalid": "Not a valid file."}
def _deserialize(self, value, attr, data, **kwargs):
print("in _deserialize")
if not isinstance(value, FileStorage):
raise self.make_error("invalid")
return value
class UploadSchema(ma.Schema):
photo = FileField()
app = flask.Flask(__name__)
@app.route("/alpha", methods=["POST"])
@use_args(UploadSchema, location="files")
def alpha(args):
print(args)
return {}
@app.route("/beta", methods=["POST"])
@use_args(UploadSchema, location="form_and_files")
def beta(args):
print(args)
return {}
with app.test_client() as client:
print("---test /alpha")
response = client.post(
"/alpha",
data={"photo": (io.BytesIO(b"file contents"), "test.txt")},
)
print("---test /beta")
response = client.post(
"/beta",
data={"photo": (io.BytesIO(b"file contents"), "test.txt")},
)
And this is the output I see:
---test /alpha
in _deserialize
{'photo': <FileStorage: 'test.txt' ('text/plain')>}
---test /beta
in _deserialize
{'photo': <FileStorage: 'test.txt' ('text/plain')>}
The /alpha
route uses files
and the /beta
route uses form_and_files
, defined just as you have it.
If _deserialize
isn't being called, I think something else must be going on.
My versions:
$ pip freeze | grep -i -E 'webargs|flask'
Flask==2.1.2
webargs==8.1.0
from webargs.
Thanks for the help. I forgot to mention that it did call the _deserialize
if the value is a file. While if the value is not a file, then the _deserialize
will be skipped:
with app.test_client() as client:
print("---test /alpha")
response = client.post(
"/alpha",
data={"photo": "string"}, # <--
)
print("---test /beta")
response = client.post(
"/beta",
data={"photo": (io.BytesIO(b"file contents"), "test.txt")},
)
But I need the _deserialize
to be called so that I can raise an error if the user doesn't pass a file to this field.
from webargs.
If I test that same demo app with a non-file post for the "file field", I'm seeing the _deserialize
method get called as expected.
i.e. With the above, if I add this snippet:
with app.test_client() as client:
response = client.post("/beta", data={"photo": "foo"})
print(response)
I see <WrapperTestResponse streamed [422 UNPROCESSABLE ENTITY]>
added to the output, meaning that the desired ValidationError was raised.
So far I've not been able to reproduce this issue, so I'm inclined to think this is a usage error. Perhaps somewhere you've used location="files"
by mistake.
If webargs
or marshmallow
is doing something wrong or unexpected, we need more information. I think the next best step is to try to create a complete and minimal reproduction similar to the sample script I shared, since that way we can run the same code and verify the behavior. Something which might help is to set required=True
on the field(s) you want to test, since that eliminates the possibility of them being completely absent from the data.
from webargs.
A moment after I wrote
Perhaps somewhere you've used
location="files"
by mistake.
I realized that this may be the cause of the issue, and I'm rereading the original.
flask.request.files
gets populated only with valid file uploads, not other strings.
So if you send data={"photo": "foo"}
using a flask test client, flask.request.files
is ImmutableMultiDict([])
(an empty multidict). That's a flask/werkzeug behavior which location="files"
in webargs
therefore inherits.
I'll reread to confirm, but I think we're seeing the same (correct/expected) behavior, but with different expectations about what should happen.
from webargs.
It's a bit weird... If I run the same script with a string as a file, it returns 200 (instead of 422), and _deserialize
has not got called.
(venv) ➜ testapispec pip freeze | grep -i -E 'webargs|flask|werkzeug'
Flask==2.1.2
webargs==8.1.0
Werkzeug==2.1.2
(venv) ➜ testapispec cat app.py
import io
import flask
import marshmallow as ma
from webargs.multidictproxy import MultiDictProxy
from webargs.flaskparser import use_args, parser
from werkzeug.datastructures import FileStorage
@parser.location_loader("form_and_files")
def load_form_and_files(request, schema):
form_and_files_data = request.files.copy()
form_and_files_data.update(request.form)
return MultiDictProxy(form_and_files_data, schema)
class FileField(ma.fields.Field):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.metadata["type"] = "string"
self.metadata["format"] = "binary"
default_error_messages = {"invalid": "Not a valid file."}
def _deserialize(self, value, attr, data, **kwargs):
print("in _deserialize")
if not isinstance(value, FileStorage):
raise self.make_error("invalid")
return value
class UploadSchema(ma.Schema):
photo = FileField()
app = flask.Flask(__name__)
@app.route("/alpha", methods=["POST"])
@use_args(UploadSchema, location="files")
def alpha(args):
print(args)
return {}
@app.route("/beta", methods=["POST"])
@use_args(UploadSchema, location="form_and_files")
def beta(args):
print(args)
return {}
with app.test_client() as client:
print("---test /alpha")
response = client.post(
"/alpha",
# data={"photo": (io.BytesIO(b"file contents"), "test.txt")},
data={"photo": "test"},
)
print(response.status_code)
print("---test /beta")
response = client.post(
"/beta",
data={"photo": (io.BytesIO(b"file contents"), "test.txt")},
)
(venv) ➜ testapispec python app.py
---test /alpha
{}
200
---test /beta
in _deserialize
{'photo': <FileStorage: 'test.txt' ('text/plain')>}
If I send the post with an API client (i.e. Insomnia) instead of Flask's test client, it still returns 200, and _deserialize
still has not got called.
This behavior only happens when I use location="files"
. It's working properly if I use the custom location form_and_files
.
In my opinion, when using location="files"
, the field's _deserialize
/_validated
should always get called no matter whether the user sends a file or a string, so I can validate it in the _deserialize
/_validated
methods.
from webargs.
Ah, I know what you mean. Since the request.files
is empty, so it's not possible to trigger the _deserialize
on fields. I finally make it work by rewriting the files
location:
@parser.location_loader('files')
def load_files(request, schema):
files_data = request.files
if not files_data: # make sure the data is not empty
files_data = request.form # the failed parsed file fields goes into request.form
return MultiDictProxy(files_data, schema)
Do you think we should change the default behavior of webargs to make sure the files
data is not empty? I just made a PR (#721) to keep this easier to understand. We can continue the discussion in that PR. Thanks!
from webargs.
@greyli, thanks for putting in the work on the PR, including looking into the problems it would cause and making the decision to self-close! The changes we don't make are as important as the ones which we do make.
Per the closing comment #721, automatically combining files
and form
would lead to problems. At the very least, it would need to be a major release (9.0), but I'm also not sure it's a good idea.
To offer a potential improvement which we could make, would a form_and_files
location be a good addition? I haven't thought about this very hard, but if you're doing this in your library it's possible it would be useful to direct webargs
consumers too. We could even define it on the base Parser
class, making it available in other frameworks -- although we'd need to think about how it behaves on each framework.
from webargs.
Related Issues (20)
- potential documentation issue in quickstart/error-handling HOT 3
- Parse a schema that contains both file and form data HOT 2
- How to strip empty strings from list of strings in args using DjangoParser? HOT 3
- Tags cleanup HOT 2
- How to use partial loading with nested field HOT 2
- aiohttp. body argument is deprecated for http web exceptions HOT 1
- Update minimum supported versions of frameworks in next major release HOT 1
- Release version 8.2.0 HOT 4
- [Question] how to use this with line item like data? HOT 2
- Is there possible add Quixote web framework support HOT 2
- [Question] How to set up the Schema that will validate against a list of allowed enum values? HOT 1
- Dealing with empty values in `DelimitedFieldMixin` HOT 2
- Being able to handle ValidationError in Flask view methods without custom error handler HOT 4
- webargs.core.Parser should describe default for unknown either as marshmallow.EXCLUDE or "exclude" HOT 1
- RFC: support passing the whole parsed arguments as a keyword argument HOT 10
- Please consider using Mapping instead of Dict in ArgsMap to avoid type checker errors HOT 7
- Parse schema without using decorators HOT 1
- How to handle JSON data always nested / wrapped in a "body" object HOT 9
- Webargs 8 Question: Getting request data from 'data' instead of 'json'? HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from webargs.