second commit

This commit is contained in:
2024-12-27 22:31:23 +09:00
parent 2353324570
commit 10a0f110ca
8819 changed files with 1307198 additions and 28 deletions

View File

@ -0,0 +1,81 @@
from __future__ import annotations
import importlib.metadata
import typing
from packaging.version import Version
from marshmallow.decorators import (
post_dump,
post_load,
pre_dump,
pre_load,
validates,
validates_schema,
)
from marshmallow.exceptions import ValidationError
from marshmallow.schema import Schema, SchemaOpts
from marshmallow.utils import EXCLUDE, INCLUDE, RAISE, missing, pprint
from . import fields
def __getattr__(name: str) -> typing.Any:
import warnings
if name == "__version__":
warnings.warn(
"The '__version__' attribute is deprecated and will be removed in"
" in a future version. Use feature detection or"
" 'importlib.metadata.version(\"marshmallow\")' instead.",
DeprecationWarning,
stacklevel=2,
)
return importlib.metadata.version("marshmallow")
if name == "__parsed_version__":
warnings.warn(
"The '__parsed_version__' attribute is deprecated and will be removed in"
" in a future version. Use feature detection or"
" 'packaging.Version(importlib.metadata.version(\"marshmallow\"))' instead.",
DeprecationWarning,
stacklevel=2,
)
return Version(importlib.metadata.version("marshmallow"))
if name == "__version_info__":
warnings.warn(
"The '__version_info__' attribute is deprecated and will be removed in"
" in a future version. Use feature detection or"
" 'packaging.Version(importlib.metadata.version(\"marshmallow\")).release' instead.",
DeprecationWarning,
stacklevel=2,
)
__parsed_version__ = Version(importlib.metadata.version("marshmallow"))
__version_info__: tuple[int, int, int] | tuple[int, int, int, str, int] = (
__parsed_version__.release # type: ignore[assignment]
)
if __parsed_version__.pre:
__version_info__ += __parsed_version__.pre # type: ignore[assignment]
return __version_info__
raise AttributeError(name)
__all__ = [
"EXCLUDE",
"INCLUDE",
"RAISE",
"Schema",
"SchemaOpts",
"fields",
"validates",
"validates_schema",
"pre_dump",
"post_dump",
"pre_load",
"post_load",
"pprint",
"ValidationError",
"missing",
]

View File

@ -0,0 +1,65 @@
"""Abstract base classes.
These are necessary to avoid circular imports between schema.py and fields.py.
.. warning::
This module is treated as private API.
Users should not need to use this module directly.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
class FieldABC(ABC):
"""Abstract base class from which all Field classes inherit."""
parent = None
name = None
root = None
@abstractmethod
def serialize(self, attr, obj, accessor=None):
pass
@abstractmethod
def deserialize(self, value):
pass
@abstractmethod
def _serialize(self, value, attr, obj, **kwargs):
pass
@abstractmethod
def _deserialize(self, value, attr, data, **kwargs):
pass
class SchemaABC(ABC):
"""Abstract base class from which all Schemas inherit."""
@abstractmethod
def dump(self, obj, *, many: bool | None = None):
pass
@abstractmethod
def dumps(self, obj, *, many: bool | None = None):
pass
@abstractmethod
def load(self, data, *, many: bool | None = None, partial=None, unknown=None):
pass
@abstractmethod
def loads(
self,
json_data,
*,
many: bool | None = None,
partial=None,
unknown=None,
**kwargs,
):
pass

View File

@ -0,0 +1,94 @@
"""A registry of :class:`Schema <marshmallow.Schema>` classes. This allows for string
lookup of schemas, which may be used with
class:`fields.Nested <marshmallow.fields.Nested>`.
.. warning::
This module is treated as private API.
Users should not need to use this module directly.
"""
from __future__ import annotations
import typing
from marshmallow.exceptions import RegistryError
if typing.TYPE_CHECKING:
from marshmallow import Schema
SchemaType = type[Schema]
# {
# <class_name>: <list of class objects>
# <module_path_to_class>: <list of class objects>
# }
_registry = {} # type: dict[str, list[SchemaType]]
def register(classname: str, cls: SchemaType) -> None:
"""Add a class to the registry of serializer classes. When a class is
registered, an entry for both its classname and its full, module-qualified
path are added to the registry.
Example: ::
class MyClass:
pass
register("MyClass", MyClass)
# Registry:
# {
# 'MyClass': [path.to.MyClass],
# 'path.to.MyClass': [path.to.MyClass],
# }
"""
# Module where the class is located
module = cls.__module__
# Full module path to the class
# e.g. user.schemas.UserSchema
fullpath = ".".join([module, classname])
# If the class is already registered; need to check if the entries are
# in the same module as cls to avoid having multiple instances of the same
# class in the registry
if classname in _registry and not any(
each.__module__ == module for each in _registry[classname]
):
_registry[classname].append(cls)
elif classname not in _registry:
_registry[classname] = [cls]
# Also register the full path
if fullpath not in _registry:
_registry.setdefault(fullpath, []).append(cls)
else:
# If fullpath does exist, replace existing entry
_registry[fullpath] = [cls]
return None
def get_class(classname: str, all: bool = False) -> list[SchemaType] | SchemaType:
"""Retrieve a class from the registry.
:raises: marshmallow.exceptions.RegistryError if the class cannot be found
or if there are multiple entries for the given class name.
"""
try:
classes = _registry[classname]
except KeyError as error:
raise RegistryError(
f"Class with name {classname!r} was not found. You may need "
"to import the class."
) from error
if len(classes) > 1:
if all:
return _registry[classname]
raise RegistryError(
f"Multiple classes with name {classname!r} "
"were found. Please use the full, "
"module-qualified path."
)
else:
return _registry[classname][0]

View File

@ -0,0 +1,233 @@
"""Decorators for registering schema pre-processing and post-processing methods.
These should be imported from the top-level `marshmallow` module.
Methods decorated with
`pre_load <marshmallow.decorators.pre_load>`, `post_load <marshmallow.decorators.post_load>`,
`pre_dump <marshmallow.decorators.pre_dump>`, `post_dump <marshmallow.decorators.post_dump>`,
and `validates_schema <marshmallow.decorators.validates_schema>` receive
``many`` as a keyword argument. In addition, `pre_load <marshmallow.decorators.pre_load>`,
`post_load <marshmallow.decorators.post_load>`,
and `validates_schema <marshmallow.decorators.validates_schema>` receive
``partial``. If you don't need these arguments, add ``**kwargs`` to your method
signature.
Example: ::
from marshmallow import (
Schema,
pre_load,
pre_dump,
post_load,
validates_schema,
validates,
fields,
ValidationError,
)
class UserSchema(Schema):
email = fields.Str(required=True)
age = fields.Integer(required=True)
@post_load
def lowerstrip_email(self, item, many, **kwargs):
item["email"] = item["email"].lower().strip()
return item
@pre_load(pass_many=True)
def remove_envelope(self, data, many, **kwargs):
namespace = "results" if many else "result"
return data[namespace]
@post_dump(pass_many=True)
def add_envelope(self, data, many, **kwargs):
namespace = "results" if many else "result"
return {namespace: data}
@validates_schema
def validate_email(self, data, **kwargs):
if len(data["email"]) < 3:
raise ValidationError("Email must be more than 3 characters", "email")
@validates("age")
def validate_age(self, data, **kwargs):
if data < 14:
raise ValidationError("Too young!")
.. note::
These decorators only work with instance methods. Class and static
methods are not supported.
.. warning::
The invocation order of decorated methods of the same type is not guaranteed.
If you need to guarantee order of different processing steps, you should put
them in the same processing method.
"""
from __future__ import annotations
import functools
from collections import defaultdict
from typing import Any, Callable, cast
PRE_DUMP = "pre_dump"
POST_DUMP = "post_dump"
PRE_LOAD = "pre_load"
POST_LOAD = "post_load"
VALIDATES = "validates"
VALIDATES_SCHEMA = "validates_schema"
class MarshmallowHook:
__marshmallow_hook__: dict[str, list[tuple[bool, Any]]] | None = None
def validates(field_name: str) -> Callable[..., Any]:
"""Register a field validator.
:param str field_name: Name of the field that the method validates.
"""
return set_hook(None, VALIDATES, field_name=field_name)
def validates_schema(
fn: Callable[..., Any] | None = None,
pass_many: bool = False,
pass_original: bool = False,
skip_on_field_errors: bool = True,
) -> Callable[..., Any]:
"""Register a schema-level validator.
By default it receives a single object at a time, transparently handling the ``many``
argument passed to the `Schema`'s :func:`~marshmallow.Schema.validate` call.
If ``pass_many=True``, the raw data (which may be a collection) is passed.
If ``pass_original=True``, the original data (before unmarshalling) will be passed as
an additional argument to the method.
If ``skip_on_field_errors=True``, this validation method will be skipped whenever
validation errors have been detected when validating fields.
.. versionchanged:: 3.0.0b1
``skip_on_field_errors`` defaults to `True`.
.. versionchanged:: 3.0.0
``partial`` and ``many`` are always passed as keyword arguments to
the decorated method.
"""
return set_hook(
fn,
VALIDATES_SCHEMA,
many=pass_many,
pass_original=pass_original,
skip_on_field_errors=skip_on_field_errors,
)
def pre_dump(
fn: Callable[..., Any] | None = None, pass_many: bool = False
) -> Callable[..., Any]:
"""Register a method to invoke before serializing an object. The method
receives the object to be serialized and returns the processed object.
By default it receives a single object at a time, transparently handling the ``many``
argument passed to the `Schema`'s :func:`~marshmallow.Schema.dump` call.
If ``pass_many=True``, the raw data (which may be a collection) is passed.
.. versionchanged:: 3.0.0
``many`` is always passed as a keyword arguments to the decorated method.
"""
return set_hook(fn, PRE_DUMP, many=pass_many)
def post_dump(
fn: Callable[..., Any] | None = None,
pass_many: bool = False,
pass_original: bool = False,
) -> Callable[..., Any]:
"""Register a method to invoke after serializing an object. The method
receives the serialized object and returns the processed object.
By default it receives a single object at a time, transparently handling the ``many``
argument passed to the `Schema`'s :func:`~marshmallow.Schema.dump` call.
If ``pass_many=True``, the raw data (which may be a collection) is passed.
If ``pass_original=True``, the original data (before serializing) will be passed as
an additional argument to the method.
.. versionchanged:: 3.0.0
``many`` is always passed as a keyword arguments to the decorated method.
"""
return set_hook(fn, POST_DUMP, many=pass_many, pass_original=pass_original)
def pre_load(
fn: Callable[..., Any] | None = None, pass_many: bool = False
) -> Callable[..., Any]:
"""Register a method to invoke before deserializing an object. The method
receives the data to be deserialized and returns the processed data.
By default it receives a single object at a time, transparently handling the ``many``
argument passed to the `Schema`'s :func:`~marshmallow.Schema.load` call.
If ``pass_many=True``, the raw data (which may be a collection) is passed.
.. versionchanged:: 3.0.0
``partial`` and ``many`` are always passed as keyword arguments to
the decorated method.
"""
return set_hook(fn, PRE_LOAD, many=pass_many)
def post_load(
fn: Callable[..., Any] | None = None,
pass_many: bool = False,
pass_original: bool = False,
) -> Callable[..., Any]:
"""Register a method to invoke after deserializing an object. The method
receives the deserialized data and returns the processed data.
By default it receives a single object at a time, transparently handling the ``many``
argument passed to the `Schema`'s :func:`~marshmallow.Schema.load` call.
If ``pass_many=True``, the raw data (which may be a collection) is passed.
If ``pass_original=True``, the original data (before deserializing) will be passed as
an additional argument to the method.
.. versionchanged:: 3.0.0
``partial`` and ``many`` are always passed as keyword arguments to
the decorated method.
"""
return set_hook(fn, POST_LOAD, many=pass_many, pass_original=pass_original)
def set_hook(
fn: Callable[..., Any] | None, tag: str, many: bool = False, **kwargs: Any
) -> Callable[..., Any]:
"""Mark decorated function as a hook to be picked up later.
You should not need to use this method directly.
.. note::
Currently only works with functions and instance methods. Class and
static methods are not supported.
:return: Decorated function if supplied, else this decorator with its args
bound.
"""
# Allow using this as either a decorator or a decorator factory.
if fn is None:
return functools.partial(set_hook, tag=tag, many=many, **kwargs)
# Set a __marshmallow_hook__ attribute instead of wrapping in some class,
# because I still want this to end up as a normal (unbound) method.
function = cast(MarshmallowHook, fn)
try:
hook_config = function.__marshmallow_hook__
except AttributeError:
function.__marshmallow_hook__ = hook_config = defaultdict(list)
# Also save the kwargs for the tagged function on
# __marshmallow_hook__, keyed by <tag>
if hook_config is not None:
hook_config[tag].append((many, kwargs))
return fn

View File

@ -0,0 +1,60 @@
"""Utilities for storing collections of error messages.
.. warning::
This module is treated as private API.
Users should not need to use this module directly.
"""
from marshmallow.exceptions import SCHEMA
class ErrorStore:
def __init__(self):
#: Dictionary of errors stored during serialization
self.errors = {}
def store_error(self, messages, field_name=SCHEMA, index=None):
# field error -> store/merge error messages under field name key
# schema error -> if string or list, store/merge under _schema key
# -> if dict, store/merge with other top-level keys
if field_name != SCHEMA or not isinstance(messages, dict):
messages = {field_name: messages}
if index is not None:
messages = {index: messages}
self.errors = merge_errors(self.errors, messages)
def merge_errors(errors1, errors2):
"""Deeply merge two error messages.
The format of ``errors1`` and ``errors2`` matches the ``message``
parameter of :exc:`marshmallow.exceptions.ValidationError`.
"""
if not errors1:
return errors2
if not errors2:
return errors1
if isinstance(errors1, list):
if isinstance(errors2, list):
return errors1 + errors2
if isinstance(errors2, dict):
return dict(errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))})
return errors1 + [errors2]
if isinstance(errors1, dict):
if isinstance(errors2, list):
return dict(errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)})
if isinstance(errors2, dict):
errors = dict(errors1)
for key, val in errors2.items():
if key in errors:
errors[key] = merge_errors(errors[key], val)
else:
errors[key] = val
return errors
return dict(errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)})
if isinstance(errors2, list):
return [errors1] + errors2
if isinstance(errors2, dict):
return dict(errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))})
return [errors1, errors2]

View File

@ -0,0 +1,71 @@
"""Exception classes for marshmallow-related errors."""
from __future__ import annotations
import typing
# Key used for schema-level validation errors
SCHEMA = "_schema"
class MarshmallowError(Exception):
"""Base class for all marshmallow-related errors."""
class ValidationError(MarshmallowError):
"""Raised when validation fails on a field or schema.
Validators and custom fields should raise this exception.
:param message: An error message, list of error messages, or dict of
error messages. If a dict, the keys are subitems and the values are error messages.
:param field_name: Field name to store the error on.
If `None`, the error is stored as schema-level error.
:param data: Raw input data.
:param valid_data: Valid (de)serialized data.
"""
def __init__(
self,
message: str | list | dict,
field_name: str = SCHEMA,
data: typing.Mapping[str, typing.Any]
| typing.Iterable[typing.Mapping[str, typing.Any]]
| None = None,
valid_data: list[dict[str, typing.Any]] | dict[str, typing.Any] | None = None,
**kwargs,
):
self.messages = [message] if isinstance(message, (str, bytes)) else message
self.field_name = field_name
self.data = data
self.valid_data = valid_data
self.kwargs = kwargs
super().__init__(message)
def normalized_messages(self):
if self.field_name == SCHEMA and isinstance(self.messages, dict):
return self.messages
return {self.field_name: self.messages}
@property
def messages_dict(self) -> dict[str, typing.Any]:
if not isinstance(self.messages, dict):
raise TypeError(
"cannot access 'messages_dict' when 'messages' is of type "
+ type(self.messages).__name__
)
return self.messages
class RegistryError(NameError):
"""Raised when an invalid operation is performed on the serializer
class registry.
"""
class StringNotCollectionError(MarshmallowError, TypeError):
"""Raised when a string is passed when a list of strings is expected."""
class FieldInstanceResolutionError(MarshmallowError, TypeError):
"""Raised when schema to instantiate is neither a Schema class nor an instance."""

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,89 @@
# OrderedSet
# Copyright (c) 2009 Raymond Hettinger
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
from collections.abc import MutableSet
class OrderedSet(MutableSet):
def __init__(self, iterable=None):
self.end = end = []
end += [None, end, end] # sentinel node for doubly linked list
self.map = {} # key --> [key, prev, next]
if iterable is not None:
self |= iterable
def __len__(self):
return len(self.map)
def __contains__(self, key):
return key in self.map
def add(self, key):
if key not in self.map:
end = self.end
curr = end[1]
curr[2] = end[1] = self.map[key] = [key, curr, end]
def discard(self, key):
if key in self.map:
key, prev, next = self.map.pop(key)
prev[2] = next
next[1] = prev
def __iter__(self):
end = self.end
curr = end[2]
while curr is not end:
yield curr[0]
curr = curr[2]
def __reversed__(self):
end = self.end
curr = end[1]
while curr is not end:
yield curr[0]
curr = curr[1]
def pop(self, last=True):
if not self:
raise KeyError("set is empty")
key = self.end[1][0] if last else self.end[2][0]
self.discard(key)
return key
def __repr__(self):
if not self:
return f"{self.__class__.__name__}()"
return f"{self.__class__.__name__}({list(self)!r})"
def __eq__(self, other):
if isinstance(other, OrderedSet):
return len(self) == len(other) and list(self) == list(other)
return set(self) == set(other)
if __name__ == "__main__":
s = OrderedSet("abracadaba")
t = OrderedSet("simsalabim")
print(s | t)
print(s & t)
print(s - t)

View File

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
"""Type aliases.
.. warning::
This module is provisional. Types may be modified, added, and removed between minor releases.
"""
import typing
StrSequenceOrSet = typing.Union[typing.Sequence[str], typing.AbstractSet[str]]
Validator = typing.Callable[[typing.Any], typing.Any]

View File

@ -0,0 +1,378 @@
"""Utility methods for marshmallow."""
from __future__ import annotations
import collections
import datetime as dt
import functools
import inspect
import json
import re
import typing
import warnings
from collections.abc import Mapping
from email.utils import format_datetime, parsedate_to_datetime
from pprint import pprint as py_pprint
from marshmallow.base import FieldABC
from marshmallow.exceptions import FieldInstanceResolutionError
from marshmallow.warnings import RemovedInMarshmallow4Warning
EXCLUDE = "exclude"
INCLUDE = "include"
RAISE = "raise"
_UNKNOWN_VALUES = {EXCLUDE, INCLUDE, RAISE}
class _Missing:
def __bool__(self):
return False
def __copy__(self):
return self
def __deepcopy__(self, _):
return self
def __repr__(self):
return "<marshmallow.missing>"
# Singleton value that indicates that a field's value is missing from input
# dict passed to :meth:`Schema.load`. If the field's value is not required,
# it's ``default`` value is used.
missing = _Missing()
def is_generator(obj) -> bool:
"""Return True if ``obj`` is a generator"""
return inspect.isgeneratorfunction(obj) or inspect.isgenerator(obj)
def is_iterable_but_not_string(obj) -> bool:
"""Return True if ``obj`` is an iterable object that isn't a string."""
return (hasattr(obj, "__iter__") and not hasattr(obj, "strip")) or is_generator(obj)
def is_collection(obj) -> bool:
"""Return True if ``obj`` is a collection type, e.g list, tuple, queryset."""
return is_iterable_but_not_string(obj) and not isinstance(obj, Mapping)
def is_instance_or_subclass(val, class_) -> bool:
"""Return True if ``val`` is either a subclass or instance of ``class_``."""
try:
return issubclass(val, class_)
except TypeError:
return isinstance(val, class_)
def is_keyed_tuple(obj) -> bool:
"""Return True if ``obj`` has keyed tuple behavior, such as
namedtuples or SQLAlchemy's KeyedTuples.
"""
return isinstance(obj, tuple) and hasattr(obj, "_fields")
def pprint(obj, *args, **kwargs) -> None:
"""Pretty-printing function that can pretty-print OrderedDicts
like regular dictionaries. Useful for printing the output of
:meth:`marshmallow.Schema.dump`.
.. deprecated:: 3.7.0
marshmallow.pprint will be removed in marshmallow 4.
"""
warnings.warn(
"marshmallow's pprint function is deprecated and will be removed in marshmallow 4.",
RemovedInMarshmallow4Warning,
stacklevel=2,
)
if isinstance(obj, collections.OrderedDict):
print(json.dumps(obj, *args, **kwargs))
else:
py_pprint(obj, *args, **kwargs)
# https://stackoverflow.com/a/27596917
def is_aware(datetime: dt.datetime) -> bool:
return (
datetime.tzinfo is not None and datetime.tzinfo.utcoffset(datetime) is not None
)
def from_rfc(datestring: str) -> dt.datetime:
"""Parse a RFC822-formatted datetime string and return a datetime object.
https://stackoverflow.com/questions/885015/how-to-parse-a-rfc-2822-date-time-into-a-python-datetime # noqa: B950
"""
return parsedate_to_datetime(datestring)
def rfcformat(datetime: dt.datetime) -> str:
"""Return the RFC822-formatted representation of a datetime object.
:param datetime datetime: The datetime.
"""
return format_datetime(datetime)
# Hat tip to Django for ISO8601 deserialization functions
_iso8601_datetime_re = re.compile(
r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
r"[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
)
_iso8601_date_re = re.compile(r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$")
_iso8601_time_re = re.compile(
r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
)
def get_fixed_timezone(offset: int | float | dt.timedelta) -> dt.timezone:
"""Return a tzinfo instance with a fixed offset from UTC."""
if isinstance(offset, dt.timedelta):
offset = offset.total_seconds() // 60
sign = "-" if offset < 0 else "+"
hhmm = "%02d%02d" % divmod(abs(offset), 60)
name = sign + hhmm
return dt.timezone(dt.timedelta(minutes=offset), name)
def from_iso_datetime(value):
"""Parse a string and return a datetime.datetime.
This function supports time zone offsets. When the input contains one,
the output uses a timezone with a fixed offset from UTC.
"""
match = _iso8601_datetime_re.match(value)
if not match:
raise ValueError("Not a valid ISO8601-formatted datetime string")
kw = match.groupdict()
kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0")
tzinfo = kw.pop("tzinfo")
if tzinfo == "Z":
tzinfo = dt.timezone.utc
elif tzinfo is not None:
offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0
offset = 60 * int(tzinfo[1:3]) + offset_mins
if tzinfo[0] == "-":
offset = -offset
tzinfo = get_fixed_timezone(offset)
kw = {k: int(v) for k, v in kw.items() if v is not None}
kw["tzinfo"] = tzinfo
return dt.datetime(**kw)
def from_iso_time(value):
"""Parse a string and return a datetime.time.
This function doesn't support time zone offsets.
"""
match = _iso8601_time_re.match(value)
if not match:
raise ValueError("Not a valid ISO8601-formatted time string")
kw = match.groupdict()
kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0")
kw = {k: int(v) for k, v in kw.items() if v is not None}
return dt.time(**kw)
def from_iso_date(value):
"""Parse a string and return a datetime.date."""
match = _iso8601_date_re.match(value)
if not match:
raise ValueError("Not a valid ISO8601-formatted date string")
kw = {k: int(v) for k, v in match.groupdict().items()}
return dt.date(**kw)
def from_timestamp(value: typing.Any) -> dt.datetime:
if value is True or value is False:
raise ValueError("Not a valid POSIX timestamp")
value = float(value)
if value < 0:
raise ValueError("Not a valid POSIX timestamp")
# Load a timestamp with utc as timezone to prevent using system timezone.
# Then set timezone to None, to let the Field handle adding timezone info.
try:
return dt.datetime.fromtimestamp(value, tz=dt.timezone.utc).replace(tzinfo=None)
except OverflowError as exc:
raise ValueError("Timestamp is too large") from exc
except OSError as exc:
raise ValueError("Error converting value to datetime") from exc
def from_timestamp_ms(value: typing.Any) -> dt.datetime:
value = float(value)
return from_timestamp(value / 1000)
def timestamp(
value: dt.datetime,
) -> float:
if not is_aware(value):
# When a date is naive, use UTC as zone info to prevent using system timezone.
value = value.replace(tzinfo=dt.timezone.utc)
return value.timestamp()
def timestamp_ms(value: dt.datetime) -> float:
return timestamp(value) * 1000
def isoformat(datetime: dt.datetime) -> str:
"""Return the ISO8601-formatted representation of a datetime object.
:param datetime datetime: The datetime.
"""
return datetime.isoformat()
def to_iso_time(time: dt.time) -> str:
return dt.time.isoformat(time)
def to_iso_date(date: dt.date) -> str:
return dt.date.isoformat(date)
def ensure_text_type(val: str | bytes) -> str:
if isinstance(val, bytes):
val = val.decode("utf-8")
return str(val)
def pluck(dictlist: list[dict[str, typing.Any]], key: str):
"""Extracts a list of dictionary values from a list of dictionaries.
::
>>> dlist = [{'id': 1, 'name': 'foo'}, {'id': 2, 'name': 'bar'}]
>>> pluck(dlist, 'id')
[1, 2]
"""
return [d[key] for d in dictlist]
# Various utilities for pulling keyed values from objects
def get_value(obj, key: int | str, default=missing):
"""Helper for pulling a keyed value off various types of objects. Fields use
this method by default to access attributes of the source object. For object `x`
and attribute `i`, this method first tries to access `x[i]`, and then falls back to
`x.i` if an exception is raised.
.. warning::
If an object `x` does not raise an exception when `x[i]` does not exist,
`get_value` will never check the value `x.i`. Consider overriding
`marshmallow.fields.Field.get_value` in this case.
"""
if not isinstance(key, int) and "." in key:
return _get_value_for_keys(obj, key.split("."), default)
else:
return _get_value_for_key(obj, key, default)
def _get_value_for_keys(obj, keys, default):
if len(keys) == 1:
return _get_value_for_key(obj, keys[0], default)
else:
return _get_value_for_keys(
_get_value_for_key(obj, keys[0], default), keys[1:], default
)
def _get_value_for_key(obj, key, default):
if not hasattr(obj, "__getitem__"):
return getattr(obj, key, default)
try:
return obj[key]
except (KeyError, IndexError, TypeError, AttributeError):
return getattr(obj, key, default)
def set_value(dct: dict[str, typing.Any], key: str, value: typing.Any):
"""Set a value in a dict. If `key` contains a '.', it is assumed
be a path (i.e. dot-delimited string) to the value's location.
::
>>> d = {}
>>> set_value(d, 'foo.bar', 42)
>>> d
{'foo': {'bar': 42}}
"""
if "." in key:
head, rest = key.split(".", 1)
target = dct.setdefault(head, {})
if not isinstance(target, dict):
raise ValueError(
f"Cannot set {key} in {head} " f"due to existing value: {target}"
)
set_value(target, rest, value)
else:
dct[key] = value
def callable_or_raise(obj):
"""Check that an object is callable, else raise a :exc:`TypeError`."""
if not callable(obj):
raise TypeError(f"Object {obj!r} is not callable.")
return obj
def _signature(func: typing.Callable) -> list[str]:
return list(inspect.signature(func).parameters.keys())
def get_func_args(func: typing.Callable) -> list[str]:
"""Given a callable, return a list of argument names. Handles
`functools.partial` objects and class-based callables.
.. versionchanged:: 3.0.0a1
Do not return bound arguments, eg. ``self``.
"""
if inspect.isfunction(func) or inspect.ismethod(func):
return _signature(func)
if isinstance(func, functools.partial):
return _signature(func.func)
# Callable class
return _signature(func)
def resolve_field_instance(cls_or_instance):
"""Return a Schema instance from a Schema class or instance.
:param type|Schema cls_or_instance: Marshmallow Schema class or instance.
"""
if isinstance(cls_or_instance, type):
if not issubclass(cls_or_instance, FieldABC):
raise FieldInstanceResolutionError
return cls_or_instance()
else:
if not isinstance(cls_or_instance, FieldABC):
raise FieldInstanceResolutionError
return cls_or_instance
def timedelta_to_microseconds(value: dt.timedelta) -> int:
"""Compute the total microseconds of a timedelta
https://github.com/python/cpython/blob/bb3e0c240bc60fe08d332ff5955d54197f79751c/Lib/datetime.py#L665-L667 # noqa: B950
"""
return (value.days * (24 * 3600) + value.seconds) * 1000000 + value.microseconds
def validate_unknown_parameter_value(obj: typing.Any) -> str:
if obj not in _UNKNOWN_VALUES:
raise ValueError(
f"Object {obj!r} is not a valid value for the 'unknown' parameter"
)
return obj

View File

@ -0,0 +1,678 @@
"""Validation classes for various types of data."""
from __future__ import annotations
import re
import typing
from abc import ABC, abstractmethod
from itertools import zip_longest
from operator import attrgetter
from marshmallow import types
from marshmallow.exceptions import ValidationError
_T = typing.TypeVar("_T")
class Validator(ABC):
"""Abstract base class for validators.
.. note::
This class does not provide any validation behavior. It is only used to
add a useful `__repr__` implementation for validators.
"""
error = None # type: str | None
def __repr__(self) -> str:
args = self._repr_args()
args = f"{args}, " if args else ""
return f"<{self.__class__.__name__}({args}error={self.error!r})>"
def _repr_args(self) -> str:
"""A string representation of the args passed to this validator. Used by
`__repr__`.
"""
return ""
@abstractmethod
def __call__(self, value: typing.Any) -> typing.Any: ...
class And(Validator):
"""Compose multiple validators and combine their error messages.
Example: ::
from marshmallow import validate, ValidationError
def is_even(value):
if value % 2 != 0:
raise ValidationError("Not an even value.")
validator = validate.And(validate.Range(min=0), is_even)
validator(-1)
# ValidationError: ['Must be greater than or equal to 0.', 'Not an even value.']
:param validators: Validators to combine.
:param error: Error message to use when a validator returns ``False``.
"""
default_error_message = "Invalid value."
def __init__(self, *validators: types.Validator, error: str | None = None):
self.validators = tuple(validators)
self.error = error or self.default_error_message # type: str
def _repr_args(self) -> str:
return f"validators={self.validators!r}"
def __call__(self, value: typing.Any) -> typing.Any:
errors = []
kwargs = {}
for validator in self.validators:
try:
r = validator(value)
if not isinstance(validator, Validator) and r is False:
raise ValidationError(self.error)
except ValidationError as err:
kwargs.update(err.kwargs)
if isinstance(err.messages, dict):
errors.append(err.messages)
else:
# FIXME : Get rid of cast
errors.extend(typing.cast(list, err.messages))
if errors:
raise ValidationError(errors, **kwargs)
return value
class URL(Validator):
"""Validate a URL.
:param relative: Whether to allow relative URLs.
:param absolute: Whether to allow absolute URLs.
:param error: Error message to raise in case of a validation error.
Can be interpolated with `{input}`.
:param schemes: Valid schemes. By default, ``http``, ``https``,
``ftp``, and ``ftps`` are allowed.
:param require_tld: Whether to reject non-FQDN hostnames.
"""
class RegexMemoizer:
def __init__(self):
self._memoized = {}
def _regex_generator(
self, relative: bool, absolute: bool, require_tld: bool
) -> typing.Pattern:
hostname_variants = [
# a normal domain name, expressed in [A-Z0-9] chars with hyphens allowed only in the middle
# note that the regex will be compiled with IGNORECASE, so these are upper and lowercase chars
(
r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+"
r"(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)"
),
# or the special string 'localhost'
r"localhost",
# or IPv4
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}",
# or IPv6
r"\[[A-F0-9]*:[A-F0-9:]+\]",
]
if not require_tld:
# allow dotless hostnames
hostname_variants.append(r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.?)")
absolute_part = "".join(
(
# scheme (e.g. 'https://', 'ftp://', etc)
# this is validated separately against allowed schemes, so in the regex
# we simply want to capture its existence
r"(?:[a-z0-9\.\-\+]*)://",
# userinfo, for URLs encoding authentication
# e.g. 'ftp://foo:bar@ftp.example.org/'
r"(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?",
# netloc, the hostname/domain part of the URL plus the optional port
r"(?:",
"|".join(hostname_variants),
r")",
r"(?::\d+)?",
)
)
relative_part = r"(?:/?|[/?]\S+)\Z"
if relative:
if absolute:
parts: tuple[str, ...] = (
r"^(",
absolute_part,
r")?",
relative_part,
)
else:
parts = (r"^", relative_part)
else:
parts = (r"^", absolute_part, relative_part)
return re.compile("".join(parts), re.IGNORECASE)
def __call__(
self, relative: bool, absolute: bool, require_tld: bool
) -> typing.Pattern:
key = (relative, absolute, require_tld)
if key not in self._memoized:
self._memoized[key] = self._regex_generator(
relative, absolute, require_tld
)
return self._memoized[key]
_regex = RegexMemoizer()
default_message = "Not a valid URL."
default_schemes = {"http", "https", "ftp", "ftps"}
def __init__(
self,
*,
relative: bool = False,
absolute: bool = True,
schemes: types.StrSequenceOrSet | None = None,
require_tld: bool = True,
error: str | None = None,
):
if not relative and not absolute:
raise ValueError(
"URL validation cannot set both relative and absolute to False."
)
self.relative = relative
self.absolute = absolute
self.error = error or self.default_message # type: str
self.schemes = schemes or self.default_schemes
self.require_tld = require_tld
def _repr_args(self) -> str:
return f"relative={self.relative!r}, absolute={self.absolute!r}"
def _format_error(self, value) -> str:
return self.error.format(input=value)
def __call__(self, value: str) -> str:
message = self._format_error(value)
if not value:
raise ValidationError(message)
# Check first if the scheme is valid
if "://" in value:
scheme = value.split("://")[0].lower()
if scheme not in self.schemes:
raise ValidationError(message)
regex = self._regex(self.relative, self.absolute, self.require_tld)
if not regex.search(value):
raise ValidationError(message)
return value
class Email(Validator):
"""Validate an email address.
:param error: Error message to raise in case of a validation error. Can be
interpolated with `{input}`.
"""
USER_REGEX = re.compile(
r"(^[-!#$%&'*+/=?^`{}|~\w]+(\.[-!#$%&'*+/=?^`{}|~\w]+)*\Z" # dot-atom
# quoted-string
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]'
r'|\\[\001-\011\013\014\016-\177])*"\Z)',
re.IGNORECASE | re.UNICODE,
)
DOMAIN_REGEX = re.compile(
# domain
r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+"
r"(?:[A-Z]{2,6}|[A-Z0-9-]{2,})\Z"
# literal form, ipv4 address (SMTP 4.1.3)
r"|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)"
r"(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]\Z",
re.IGNORECASE | re.UNICODE,
)
DOMAIN_WHITELIST = ("localhost",)
default_message = "Not a valid email address."
def __init__(self, *, error: str | None = None):
self.error = error or self.default_message # type: str
def _format_error(self, value: str) -> str:
return self.error.format(input=value)
def __call__(self, value: str) -> str:
message = self._format_error(value)
if not value or "@" not in value:
raise ValidationError(message)
user_part, domain_part = value.rsplit("@", 1)
if not self.USER_REGEX.match(user_part):
raise ValidationError(message)
if domain_part not in self.DOMAIN_WHITELIST:
if not self.DOMAIN_REGEX.match(domain_part):
try:
domain_part = domain_part.encode("idna").decode("ascii")
except UnicodeError:
pass
else:
if self.DOMAIN_REGEX.match(domain_part):
return value
raise ValidationError(message)
return value
class Range(Validator):
"""Validator which succeeds if the value passed to it is within the specified
range. If ``min`` is not specified, or is specified as `None`,
no lower bound exists. If ``max`` is not specified, or is specified as `None`,
no upper bound exists. The inclusivity of the bounds (if they exist) is configurable.
If ``min_inclusive`` is not specified, or is specified as `True`, then
the ``min`` bound is included in the range. If ``max_inclusive`` is not specified,
or is specified as `True`, then the ``max`` bound is included in the range.
:param min: The minimum value (lower bound). If not provided, minimum
value will not be checked.
:param max: The maximum value (upper bound). If not provided, maximum
value will not be checked.
:param min_inclusive: Whether the `min` bound is included in the range.
:param max_inclusive: Whether the `max` bound is included in the range.
:param error: Error message to raise in case of a validation error.
Can be interpolated with `{input}`, `{min}` and `{max}`.
"""
message_min = "Must be {min_op} {{min}}."
message_max = "Must be {max_op} {{max}}."
message_all = "Must be {min_op} {{min}} and {max_op} {{max}}."
message_gte = "greater than or equal to"
message_gt = "greater than"
message_lte = "less than or equal to"
message_lt = "less than"
def __init__(
self,
min=None,
max=None,
*,
min_inclusive: bool = True,
max_inclusive: bool = True,
error: str | None = None,
):
self.min = min
self.max = max
self.error = error
self.min_inclusive = min_inclusive
self.max_inclusive = max_inclusive
# interpolate messages based on bound inclusivity
self.message_min = self.message_min.format(
min_op=self.message_gte if self.min_inclusive else self.message_gt
)
self.message_max = self.message_max.format(
max_op=self.message_lte if self.max_inclusive else self.message_lt
)
self.message_all = self.message_all.format(
min_op=self.message_gte if self.min_inclusive else self.message_gt,
max_op=self.message_lte if self.max_inclusive else self.message_lt,
)
def _repr_args(self) -> str:
return f"min={self.min!r}, max={self.max!r}, min_inclusive={self.min_inclusive!r}, max_inclusive={self.max_inclusive!r}"
def _format_error(self, value: _T, message: str) -> str:
return (self.error or message).format(input=value, min=self.min, max=self.max)
def __call__(self, value: _T) -> _T:
if self.min is not None and (
value < self.min if self.min_inclusive else value <= self.min
):
message = self.message_min if self.max is None else self.message_all
raise ValidationError(self._format_error(value, message))
if self.max is not None and (
value > self.max if self.max_inclusive else value >= self.max
):
message = self.message_max if self.min is None else self.message_all
raise ValidationError(self._format_error(value, message))
return value
class Length(Validator):
"""Validator which succeeds if the value passed to it has a
length between a minimum and maximum. Uses len(), so it
can work for strings, lists, or anything with length.
:param min: The minimum length. If not provided, minimum length
will not be checked.
:param max: The maximum length. If not provided, maximum length
will not be checked.
:param equal: The exact length. If provided, maximum and minimum
length will not be checked.
:param error: Error message to raise in case of a validation error.
Can be interpolated with `{input}`, `{min}` and `{max}`.
"""
message_min = "Shorter than minimum length {min}."
message_max = "Longer than maximum length {max}."
message_all = "Length must be between {min} and {max}."
message_equal = "Length must be {equal}."
def __init__(
self,
min: int | None = None,
max: int | None = None,
*,
equal: int | None = None,
error: str | None = None,
):
if equal is not None and any([min, max]):
raise ValueError(
"The `equal` parameter was provided, maximum or "
"minimum parameter must not be provided."
)
self.min = min
self.max = max
self.error = error
self.equal = equal
def _repr_args(self) -> str:
return f"min={self.min!r}, max={self.max!r}, equal={self.equal!r}"
def _format_error(self, value: typing.Sized, message: str) -> str:
return (self.error or message).format(
input=value, min=self.min, max=self.max, equal=self.equal
)
def __call__(self, value: typing.Sized) -> typing.Sized:
length = len(value)
if self.equal is not None:
if length != self.equal:
raise ValidationError(self._format_error(value, self.message_equal))
return value
if self.min is not None and length < self.min:
message = self.message_min if self.max is None else self.message_all
raise ValidationError(self._format_error(value, message))
if self.max is not None and length > self.max:
message = self.message_max if self.min is None else self.message_all
raise ValidationError(self._format_error(value, message))
return value
class Equal(Validator):
"""Validator which succeeds if the ``value`` passed to it is
equal to ``comparable``.
:param comparable: The object to compare to.
:param error: Error message to raise in case of a validation error.
Can be interpolated with `{input}` and `{other}`.
"""
default_message = "Must be equal to {other}."
def __init__(self, comparable, *, error: str | None = None):
self.comparable = comparable
self.error = error or self.default_message # type: str
def _repr_args(self) -> str:
return f"comparable={self.comparable!r}"
def _format_error(self, value: _T) -> str:
return self.error.format(input=value, other=self.comparable)
def __call__(self, value: _T) -> _T:
if value != self.comparable:
raise ValidationError(self._format_error(value))
return value
class Regexp(Validator):
"""Validator which succeeds if the ``value`` matches ``regex``.
.. note::
Uses `re.match`, which searches for a match at the beginning of a string.
:param regex: The regular expression string to use. Can also be a compiled
regular expression pattern.
:param flags: The regexp flags to use, for example re.IGNORECASE. Ignored
if ``regex`` is not a string.
:param error: Error message to raise in case of a validation error.
Can be interpolated with `{input}` and `{regex}`.
"""
default_message = "String does not match expected pattern."
def __init__(
self,
regex: str | bytes | typing.Pattern,
flags: int = 0,
*,
error: str | None = None,
):
self.regex = (
re.compile(regex, flags) if isinstance(regex, (str, bytes)) else regex
)
self.error = error or self.default_message # type: str
def _repr_args(self) -> str:
return f"regex={self.regex!r}"
def _format_error(self, value: str | bytes) -> str:
return self.error.format(input=value, regex=self.regex.pattern)
@typing.overload
def __call__(self, value: str) -> str: ...
@typing.overload
def __call__(self, value: bytes) -> bytes: ...
def __call__(self, value):
if self.regex.match(value) is None:
raise ValidationError(self._format_error(value))
return value
class Predicate(Validator):
"""Call the specified ``method`` of the ``value`` object. The
validator succeeds if the invoked method returns an object that
evaluates to True in a Boolean context. Any additional keyword
argument will be passed to the method.
:param method: The name of the method to invoke.
:param error: Error message to raise in case of a validation error.
Can be interpolated with `{input}` and `{method}`.
:param kwargs: Additional keyword arguments to pass to the method.
"""
default_message = "Invalid input."
def __init__(self, method: str, *, error: str | None = None, **kwargs):
self.method = method
self.error = error or self.default_message # type: str
self.kwargs = kwargs
def _repr_args(self) -> str:
return f"method={self.method!r}, kwargs={self.kwargs!r}"
def _format_error(self, value: typing.Any) -> str:
return self.error.format(input=value, method=self.method)
def __call__(self, value: typing.Any) -> typing.Any:
method = getattr(value, self.method)
if not method(**self.kwargs):
raise ValidationError(self._format_error(value))
return value
class NoneOf(Validator):
"""Validator which fails if ``value`` is a member of ``iterable``.
:param iterable: A sequence of invalid values.
:param error: Error message to raise in case of a validation error. Can be
interpolated using `{input}` and `{values}`.
"""
default_message = "Invalid input."
def __init__(self, iterable: typing.Iterable, *, error: str | None = None):
self.iterable = iterable
self.values_text = ", ".join(str(each) for each in self.iterable)
self.error = error or self.default_message # type: str
def _repr_args(self) -> str:
return f"iterable={self.iterable!r}"
def _format_error(self, value) -> str:
return self.error.format(input=value, values=self.values_text)
def __call__(self, value: typing.Any) -> typing.Any:
try:
if value in self.iterable:
raise ValidationError(self._format_error(value))
except TypeError:
pass
return value
class OneOf(Validator):
"""Validator which succeeds if ``value`` is a member of ``choices``.
:param choices: A sequence of valid values.
:param labels: Optional sequence of labels to pair with the choices.
:param error: Error message to raise in case of a validation error. Can be
interpolated with `{input}`, `{choices}` and `{labels}`.
"""
default_message = "Must be one of: {choices}."
def __init__(
self,
choices: typing.Iterable,
labels: typing.Iterable[str] | None = None,
*,
error: str | None = None,
):
self.choices = choices
self.choices_text = ", ".join(str(choice) for choice in self.choices)
self.labels = labels if labels is not None else []
self.labels_text = ", ".join(str(label) for label in self.labels)
self.error = error or self.default_message # type: str
def _repr_args(self) -> str:
return f"choices={self.choices!r}, labels={self.labels!r}"
def _format_error(self, value) -> str:
return self.error.format(
input=value, choices=self.choices_text, labels=self.labels_text
)
def __call__(self, value: typing.Any) -> typing.Any:
try:
if value not in self.choices:
raise ValidationError(self._format_error(value))
except TypeError as error:
raise ValidationError(self._format_error(value)) from error
return value
def options(
self,
valuegetter: str | typing.Callable[[typing.Any], typing.Any] = str,
) -> typing.Iterable[tuple[typing.Any, str]]:
"""Return a generator over the (value, label) pairs, where value
is a string associated with each choice. This convenience method
is useful to populate, for instance, a form select field.
:param valuegetter: Can be a callable or a string. In the former case, it must
be a one-argument callable which returns the value of a
choice. In the latter case, the string specifies the name
of an attribute of the choice objects. Defaults to `str()`
or `str()`.
"""
valuegetter = valuegetter if callable(valuegetter) else attrgetter(valuegetter)
pairs = zip_longest(self.choices, self.labels, fillvalue="")
return ((valuegetter(choice), label) for choice, label in pairs)
class ContainsOnly(OneOf):
"""Validator which succeeds if ``value`` is a sequence and each element
in the sequence is also in the sequence passed as ``choices``. Empty input
is considered valid.
:param iterable choices: Same as :class:`OneOf`.
:param iterable labels: Same as :class:`OneOf`.
:param str error: Same as :class:`OneOf`.
.. versionchanged:: 3.0.0b2
Duplicate values are considered valid.
.. versionchanged:: 3.0.0b2
Empty input is considered valid. Use `validate.Length(min=1) <marshmallow.validate.Length>`
to validate against empty inputs.
"""
default_message = "One or more of the choices you made was not in: {choices}."
def _format_error(self, value) -> str:
value_text = ", ".join(str(val) for val in value)
return super()._format_error(value_text)
def __call__(self, value: typing.Sequence[_T]) -> typing.Sequence[_T]:
# We can't use set.issubset because does not handle unhashable types
for val in value:
if val not in self.choices:
raise ValidationError(self._format_error(value))
return value
class ContainsNoneOf(NoneOf):
"""Validator which fails if ``value`` is a sequence and any element
in the sequence is a member of the sequence passed as ``iterable``. Empty input
is considered valid.
:param iterable iterable: Same as :class:`NoneOf`.
:param str error: Same as :class:`NoneOf`.
.. versionadded:: 3.6.0
"""
default_message = "One or more of the choices you made was in: {values}."
def _format_error(self, value) -> str:
value_text = ", ".join(str(val) for val in value)
return super()._format_error(value_text)
def __call__(self, value: typing.Sequence[_T]) -> typing.Sequence[_T]:
for val in value:
if val in self.iterable:
raise ValidationError(self._format_error(value))
return value

View File

@ -0,0 +1,2 @@
class RemovedInMarshmallow4Warning(DeprecationWarning):
pass