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
5 changes: 4 additions & 1 deletion schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
obtained from config-files, forms, external services or command-line
parsing, converted from JSON/YAML (or something else) to Python data-types."""

import copy
import inspect
import re
from typing import (
Expand Down Expand Up @@ -554,7 +555,9 @@ def validate(self, data: Any, **kwargs: Dict[str, Any]) -> Any:
new[default.key] = (
_invoke_with_optional_kwargs(default.default, **kwargs)
if callable(default.default)
else default.default
# Copy non-callable defaults so a mutable default (e.g. [] or
# {}) is not shared across validate() calls.
else copy.deepcopy(default.default)
)

return new
Expand Down
11 changes: 11 additions & 0 deletions test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,17 @@ def test_dict_optional_defaults():
Optional(And(str, Use(int)), default=7)


def test_dict_optional_mutable_default_not_shared():
# A mutable default (e.g. [] or {}) must not be shared across validate()
# calls: mutating one result must not leak into later ones. See GH-352.
s = Schema({Optional("items", default=[]): list})
a = s.validate({})
a["items"].append(1)
b = s.validate({})
assert b["items"] == []
assert a["items"] is not b["items"]


def test_dict_subtypes():
d = defaultdict(int, key=1)
v = Schema({"key": 1}).validate(d)
Expand Down
Loading