Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
marshmallow-version: ["marshmallow>=3.13,<4", "marshmallow>=4"]

steps:
- uses: actions/checkout@v4
Expand All @@ -31,6 +32,9 @@ jobs:
- name: Install dependencies
run: make install

- name: Install marshmallow ${{ matrix.marshmallow-version }}
run: .venv/bin/pip install "${{ matrix.marshmallow-version }}"

- name: Run unit tests
run: make test

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dev = [
"pytest-cache<2",
"pytest-cov<7",
"WebTest<4",
"marshmallow<4",
"marshmallow>=3.13",
"colander<3",
]

Expand Down
2 changes: 2 additions & 0 deletions src/cornice/validators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from cornice.validators._colander import querystring_validator as colander_querystring_validator
from cornice.validators._colander import validator as colander_validator
from cornice.validators._marshmallow import body_validator as marshmallow_body_validator
from cornice.validators._marshmallow import cornice_request
from cornice.validators._marshmallow import headers_validator as marshmallow_headers_validator
from cornice.validators._marshmallow import path_validator as marshmallow_path_validator
from cornice.validators._marshmallow import (
Expand All @@ -30,6 +31,7 @@
"marshmallow_headers_validator",
"marshmallow_path_validator",
"marshmallow_querystring_validator",
"cornice_request",
"extract_cstruct",
"DEFAULT_VALIDATORS",
"DEFAULT_FILTERS",
Expand Down
36 changes: 12 additions & 24 deletions src/cornice/validators/_marshmallow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
# You can obtain one at http://mozilla.org/MPL/2.0/.

import inspect
from contextvars import ContextVar

from cornice.validators._marshmallow_compat import EXCLUDE, set_schema_context


cornice_request: ContextVar = ContextVar("cornice_request")


def _generate_marshmallow_validator(location):
Expand Down Expand Up @@ -40,7 +46,6 @@ def _validator(request, schema=None, deserializer=None, **kwargs):
"""
import marshmallow
import marshmallow.schema
from marshmallow.utils import EXCLUDE

if schema is None:
return
Expand All @@ -51,13 +56,12 @@ def _validator(request, schema=None, deserializer=None, **kwargs):

class ValidatedField(marshmallow.fields.Field):
def _deserialize(self, value, attr, data, **kwargs):
schema.context.setdefault("request", request)
cornice_request.set(request)
set_schema_context(schema, "request", request)
deserialized = schema.load(value)
return deserialized

class Meta(object):
strict = True
ordered = True
unknown = EXCLUDE

class RequestSchemaMeta(marshmallow.schema.SchemaMeta):
Expand All @@ -80,7 +84,7 @@ def __new__(cls, name, bases, class_attrs):
"""

class_attrs[location] = ValidatedField(
required=True, metadata={"load_from": location}
required=True, data_key=location
)
class_attrs["Meta"] = Meta
return type(name, bases, class_attrs)
Expand All @@ -102,22 +106,6 @@ class RequestSchema(marshmallow.Schema, metaclass=RequestSchemaMeta): # noqa
querystring_validator = _generate_marshmallow_validator("querystring")


def _message_normalizer(exc, no_field_name="_schema"):
"""
Normally `normalize_messages` will exist on `ValidationError` but pre 2.10
versions don't have it
:param exc:
:param no_field_name:
:return:
"""
if isinstance(exc.messages, dict):
return exc.messages
field_names = exc.kwargs.get("field_names", [])
if len(field_names) == 0:
return {no_field_name: exc.messages}
return dict((name, exc.messages) for name in field_names)


def validator(request, schema=None, deserializer=None, **kwargs):
"""
Validate the full request against the schema defined on the service.
Expand Down Expand Up @@ -148,14 +136,14 @@ def validator(request, schema=None, deserializer=None, **kwargs):
return

schema = _instantiate_schema(schema)
schema.context.setdefault("request", request)
cornice_request.set(request)
set_schema_context(schema, "request", request)

cstruct = deserializer(request)
try:
deserialized = schema.load(cstruct)
except marshmallow.ValidationError as err:
# translate = request.localizer.translate
normalized_errors = _message_normalizer(err)
normalized_errors = err.messages
for location, details in normalized_errors.items():
location = location if location != "_schema" else ""
if hasattr(details, "items"):
Expand Down
22 changes: 22 additions & 0 deletions src/cornice/validators/_marshmallow_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

from importlib.metadata import version

import marshmallow


MARSHMALLOW_V4 = int(version("marshmallow").split(".")[0]) >= 4

# EXCLUDE moved from marshmallow.utils to marshmallow in v4
if MARSHMALLOW_V4:
from marshmallow import EXCLUDE
else:
from marshmallow.utils import EXCLUDE


def set_schema_context(schema, key, value):
"""Set context on schema. Uses Schema.context on v3, no-op on v4."""
if not MARSHMALLOW_V4:
schema.context.setdefault(key, value)
16 changes: 0 additions & 16 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,22 +783,6 @@ def test_no_body_schema(self):
self.assertEqual(request.validated, mock.sentinel.validated)
self.assertEqual(len(request.errors), 0)

def test_message_normalizer_no_field_names(self):
from marshmallow.exceptions import ValidationError

from cornice.validators._marshmallow import _message_normalizer

parsed = _message_normalizer(ValidationError("Test message"))
self.assertEqual({"_schema": ["Test message"]}, parsed)

def test_message_normalizer_field_names(self):
from marshmallow.exceptions import ValidationError

from cornice.validators._marshmallow import _message_normalizer

parsed = _message_normalizer(ValidationError("Test message", field_names=["test"]))
self.assertEqual({"test": ["Test message"]}, parsed)

def test_instantiated_schema(self):
app = TestApp(main({}))
with self.assertRaises(ValueError):
Expand Down
31 changes: 9 additions & 22 deletions tests/validationapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,8 @@ def form(request):
try:
import marshmallow

try:
from marshmallow.utils import EXCLUDE
except ImportError:
EXCLUDE = "exclude"
from cornice.validators import marshmallow_body_validator, marshmallow_validator
from cornice.validators import cornice_request, marshmallow_body_validator, marshmallow_validator
from cornice.validators._marshmallow_compat import EXCLUDE

MARSHMALLOW = True
except ImportError:
Expand All @@ -355,7 +352,6 @@ def form(request):

class MSignupSchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

username = marshmallow.fields.String()
Expand All @@ -364,16 +360,16 @@ class Meta:

class MNeedsContextSchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

somefield = marshmallow.fields.Float(missing=lambda: random.random())
somefield = marshmallow.fields.Float(load_default=lambda: random.random())
csrf_secret = marshmallow.fields.String()

@marshmallow.validates_schema
def validate_csrf_secret(self, data, **kwargs):
# simulate validation of session variables
if self.context["request"].get_csrf() != data.get("csrf_secret"):
# cornice_request contextvar works on both v3 and v4
if cornice_request.get().get_csrf() != data.get("csrf_secret"):
raise marshmallow.ValidationError("Wrong token")

@m_bound.post(schema=MNeedsContextSchema, validators=(marshmallow_body_validator,))
Expand Down Expand Up @@ -402,26 +398,23 @@ def m_validate_bar(node, value):

class MBodySchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

# foo and bar are required, baz is optional
foo = marshmallow.fields.String()
bar = SchemaNode(String(), validator=m_validate_bar)
baz = marshmallow.fields.String(missing=None)
ipsum = marshmallow.fields.Integer(missing=1, validate=marshmallow.validate.Range(0, 3))
baz = marshmallow.fields.String(load_default=None)
ipsum = marshmallow.fields.Integer(load_default=1, validate=marshmallow.validate.Range(0, 3))
integers = marshmallow.fields.List(marshmallow.fields.Integer())

class MQuery(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

yeah = marshmallow.fields.String()

class MRequestSchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

body = marshmallow.fields.Nested(MBodySchema)
Expand All @@ -432,7 +425,7 @@ def m_foobar_post(request):
return {"test": "succeeded"}

class MListQuerystringSequenced(marshmallow.Schema):
field = marshmallow.fields.List(marshmallow.fields.String(), many=True)
field = marshmallow.fields.List(marshmallow.fields.String())

@marshmallow.pre_load()
def normalize_field(self, data, **kwargs):
Expand All @@ -442,7 +435,6 @@ def normalize_field(self, data, **kwargs):

class MQSSchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

querystring = marshmallow.fields.Nested(MListQuerystringSequenced)
Expand All @@ -453,14 +445,12 @@ def m_foobaz_get(request):

class MNewsletterSchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

email = marshmallow.fields.String(validate=marshmallow.validate.Email())

class MRefererSchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

ref = marshmallow.fields.Integer()
Expand All @@ -485,14 +475,12 @@ def m_newsletter(request):

class MItemPathSchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

item_id = marshmallow.fields.Integer(missing=None)
item_id = marshmallow.fields.Integer(load_default=None)

class MItemSchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

path = marshmallow.fields.Nested(MItemPathSchema)
Expand All @@ -507,7 +495,6 @@ def m_item_fails(request):

class MFormSchema(marshmallow.Schema):
class Meta:
strict = True
unknown = EXCLUDE

field1 = marshmallow.fields.String()
Expand Down
25 changes: 25 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[tox]
envlist = marshmallow3, marshmallow4

[testenv]
pip_pre = false
deps =
pytest<9
pytest-cache<2
pytest-cov<7
WebTest<4
colander<3

[testenv:marshmallow3]
deps =
{[testenv]deps}
marshmallow>=3.13,<4
commands =
pytest tests/ {posargs:-v}

[testenv:marshmallow4]
deps =
{[testenv]deps}
marshmallow>=4
commands =
pytest tests/ {posargs:-v}
Loading