diff --git a/pydantic/utils.py b/pydantic/utils.py index 2c0f468e63e..40ac57dcf7a 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -225,6 +225,22 @@ def to_camel(string: str) -> str: def update_normalized_all( item: Union['AbstractSetIntStr', 'MappingIntStrAny'], all_items: Union['AbstractSetIntStr', 'MappingIntStrAny'], ) -> Union['AbstractSetIntStr', 'MappingIntStrAny']: + """ + Update item based on what all items contains. + + The update is done based on these cases: + + - if both arguments are dicts then each key-value pair existing in ``all_items`` is merged into ``item``, + while the rest of the key-value pairs are updated recursively with this function. + - if both arguments are sets then they are just merged. + - if ``item`` is a dictionary and ``all_items`` is a set then all values of it are added to ``item`` as + ``key: ...``. + - if ``item`` is set and ``all_items`` is a dictionary, then ``item`` is converted to a dictionary and then the + key-value pairs of ``all_items`` are merged in it. + + During recursive calls, there is a case where ``all_items`` can be an Ellipsis, in which case the ``item`` is + returned as is. + """ if not item: return all_items if isinstance(item, dict) and isinstance(all_items, dict): @@ -462,17 +478,16 @@ def _normalize_indexes( raise TypeError(f'Unexpected type of exclude value for index "{i}" {v.__class__}') normalized_items = {v_length + i if i < 0 else i: v for i, v in items.items() if i != '__all__'} if all_items: - default: Type[Union[set, dict]] # type: ignore + default: Type[Union[Set[Any], Dict[Any, Any]]] if isinstance(all_items, Mapping): default = dict elif isinstance(all_items, AbstractSet): default = set else: - default = ... - for i in range(v_length): - if default is ...: + for i in range(v_length): normalized_items.setdefault(i, ...) - continue + return normalized_items + for i in range(v_length): normalized_item = normalized_items.setdefault(i, default()) if normalized_item is not ...: normalized_items[i] = update_normalized_all(normalized_item, all_items) diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 4d72c28841e..e7cce3ab921 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -509,7 +509,64 @@ class Model(BaseModel): } -def test_advanced_exclude_nested_lists(): +@pytest.mark.parametrize( + 'exclude,expected', + [ + # Normal nested __all__ + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i'}}}}}, + {'subs': [{'k': 1, 'subsubs': [{'j': 1}, {'j': 2}]}, {'k': 2, 'subsubs': [{'j': 3}]}]}, + ), + # Merge sub dicts + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i'}}}, 0: {'subsubs': {'__all__': {'j'}}}}}, + {'subs': [{'k': 1, 'subsubs': [{}, {}]}, {'k': 2, 'subsubs': [{'j': 3}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs': ...}, 0: {'subsubs': {'__all__': {'j'}}}}}, + {'subs': [{'k': 1, 'subsubs': [{'i': 1}, {'i': 2}]}, {'k': 2}]}, + ), + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'j'}}}, 0: {'subsubs': ...}}}, + {'subs': [{'k': 1}, {'k': 2, 'subsubs': [{'i': 3}]}]}, + ), + # Merge sub sets + ( + {'subs': {'__all__': {'subsubs': {0}}, 0: {'subsubs': {1}}}}, + {'subs': [{'k': 1, 'subsubs': []}, {'k': 2, 'subsubs': []}]}, + ), + # Merge sub dict-set + ( + {'subs': {'__all__': {'subsubs': {0: {'i'}}}, 0: {'subsubs': {1}}}}, + {'subs': [{'k': 1, 'subsubs': [{'j': 1}]}, {'k': 2, 'subsubs': [{'j': 3}]}]}, + ), + # Different keys + ({'subs': {'__all__': {'subsubs'}, 0: {'k'}}}, {'subs': [{}, {'k': 2}]}), + ({'subs': {'__all__': {'subsubs': ...}, 0: {'k'}}}, {'subs': [{}, {'k': 2}]}), + ({'subs': {'__all__': {'subsubs'}, 0: {'k': ...}}}, {'subs': [{}, {'k': 2}]}), + # Nested different keys + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i'}, 0: {'j'}}}}}, + {'subs': [{'k': 1, 'subsubs': [{}, {'j': 2}]}, {'k': 2, 'subsubs': [{}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i': ...}, 0: {'j'}}}}}, + {'subs': [{'k': 1, 'subsubs': [{}, {'j': 2}]}, {'k': 2, 'subsubs': [{}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i'}, 0: {'j': ...}}}}}, + {'subs': [{'k': 1, 'subsubs': [{}, {'j': 2}]}, {'k': 2, 'subsubs': [{}]}]}, + ), + # Ignore __all__ for index with defined exclude + ( + {'subs': {'__all__': {'subsubs'}, 0: {'subsubs': {'__all__': {'j'}}}}}, + {'subs': [{'k': 1, 'subsubs': [{'i': 1}, {'i': 2}]}, {'k': 2}]}, + ), + ({'subs': {'__all__': {'subsubs': {'__all__': {'j'}}}, 0: ...}}, {'subs': [{'k': 2, 'subsubs': [{'i': 3}]}]}), + ({'subs': {'__all__': ..., 0: {'subsubs'}}}, {'subs': [{'k': 1}]}), + ], +) +def test_advanced_exclude_nested_lists(exclude, expected): class SubSubModel(BaseModel): i: int j: int @@ -521,60 +578,84 @@ class SubModel(BaseModel): class Model(BaseModel): subs: List[SubModel] - m = Model( - subs=[ - SubModel(k=1, subsubs=[SubSubModel(i=1, j=1), SubSubModel(i=2, j=2)]), - SubModel(k=2, subsubs=[SubSubModel(i=3, j=3)]), - ] - ) + m = Model(subs=[dict(k=1, subsubs=[dict(i=1, j=1), dict(i=2, j=2)]), dict(k=2, subsubs=[dict(i=3, j=3)])]) - # Normal nested __all__ - assert m.dict(exclude={'subs': {'__all__': {'subsubs': {'__all__': {'i'}}}}}) == { - 'subs': [{'k': 1, 'subsubs': [{'j': 1}, {'j': 2}]}, {'k': 2, 'subsubs': [{'j': 3}]}] - } - # Merge sub dicts - assert m.dict( - exclude={'subs': {'__all__': {'subsubs': {'__all__': {'i'}}}, 0: {'subsubs': {'__all__': {'j'}}}}} - ) == {'subs': [{'k': 1, 'subsubs': [{}, {}]}, {'k': 2, 'subsubs': [{'j': 3}]}]} - assert m.dict(exclude={'subs': {'__all__': {'subsubs': ...}, 0: {'subsubs': {'__all__': {'j'}}}}}) == { - 'subs': [{'k': 1, 'subsubs': [{'i': 1}, {'i': 2}]}, {'k': 2}] - } - assert m.dict(exclude={'subs': {'__all__': {'subsubs': {'__all__': {'j'}}}, 0: {'subsubs': ...}}}) == { - 'subs': [{'k': 1}, {'k': 2, 'subsubs': [{'i': 3}]}] - } - # Merge sub sets - assert m.dict(exclude={'subs': {'__all__': {'subsubs': {0}}, 0: {'subsubs': {1}}}}) == { - 'subs': [{'k': 1, 'subsubs': []}, {'k': 2, 'subsubs': []}] - } - # Merge sub dict-set - assert m.dict(exclude={'subs': {'__all__': {'subsubs': {0: {'i'}}}, 0: {'subsubs': {1}}}}) == { - 'subs': [{'k': 1, 'subsubs': [{'j': 1}]}, {'k': 2, 'subsubs': [{'j': 3}]}] - } - # Different keys - assert m.dict(exclude={'subs': {'__all__': {'subsubs'}, 0: {'k'}}}) == {'subs': [{}, {'k': 2}]} - assert m.dict(exclude={'subs': {'__all__': {'subsubs': ...}, 0: {'k'}}}) == {'subs': [{}, {'k': 2}]} - assert m.dict(exclude={'subs': {'__all__': {'subsubs'}, 0: {'k': ...}}}) == {'subs': [{}, {'k': 2}]} - # Nested different keys - assert m.dict(exclude={'subs': {'__all__': {'subsubs': {'__all__': {'i'}, 0: {'j'}}}}}) == { - 'subs': [{'k': 1, 'subsubs': [{}, {'j': 2}]}, {'k': 2, 'subsubs': [{}]}] - } - assert m.dict(exclude={'subs': {'__all__': {'subsubs': {'__all__': {'i': ...}, 0: {'j'}}}}}) == { - 'subs': [{'k': 1, 'subsubs': [{}, {'j': 2}]}, {'k': 2, 'subsubs': [{}]}] - } - assert m.dict(exclude={'subs': {'__all__': {'subsubs': {'__all__': {'i'}, 0: {'j': ...}}}}}) == { - 'subs': [{'k': 1, 'subsubs': [{}, {'j': 2}]}, {'k': 2, 'subsubs': [{}]}] - } - # Ignore __all__ for index with defined exclude. - assert m.dict(exclude={'subs': {'__all__': {'subsubs'}, 0: {'subsubs': {'__all__': {'j'}}}}}) == { - 'subs': [{'k': 1, 'subsubs': [{'i': 1}, {'i': 2}]}, {'k': 2}] - } - assert m.dict(exclude={'subs': {'__all__': {'subsubs': {'__all__': {'j'}}}, 0: ...}}) == { - 'subs': [{'k': 2, 'subsubs': [{'i': 3}]}] - } - assert m.dict(exclude={'subs': {'__all__': ..., 0: {'subsubs'}}}) == {'subs': [{'k': 1}]} + assert m.dict(exclude=exclude) == expected -def test_advanced_include_nested_lists(): +@pytest.mark.parametrize( + 'include,expected', + [ + # Normal nested __all__ + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i'}}}}}, + {'subs': [{'subsubs': [{'i': 1}, {'i': 2}]}, {'subsubs': [{'i': 3}]}]}, + ), + # Merge sub dicts + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i'}}}, 0: {'subsubs': {'__all__': {'j'}}}}}, + {'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs': ...}, 0: {'subsubs': {'__all__': {'j'}}}}}, + {'subs': [{'subsubs': [{'j': 1}, {'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'j'}}}, 0: {'subsubs': ...}}}, + {'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'j': 3}]}]}, + ), + # Merge sub sets + ( + {'subs': {'__all__': {'subsubs': {0}}, 0: {'subsubs': {1}}}}, + {'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + # Merge sub dict-set + ( + {'subs': {'__all__': {'subsubs': {0: {'i'}}}, 0: {'subsubs': {1}}}}, + {'subs': [{'subsubs': [{'i': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3}]}]}, + ), + # Different keys + ( + {'subs': {'__all__': {'subsubs'}, 0: {'k'}}}, + {'subs': [{'k': 1, 'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs': ...}, 0: {'k'}}}, + {'subs': [{'k': 1, 'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs'}, 0: {'k': ...}}}, + {'subs': [{'k': 1, 'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + # Nested different keys + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i'}, 0: {'j'}}}}}, + {'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i': ...}, 0: {'j'}}}}}, + {'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'i'}, 0: {'j': ...}}}}}, + {'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + # Ignore __all__ for index with defined include + ( + {'subs': {'__all__': {'subsubs'}, 0: {'subsubs': {'__all__': {'j'}}}}}, + {'subs': [{'subsubs': [{'j': 1}, {'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + ( + {'subs': {'__all__': {'subsubs': {'__all__': {'j'}}}, 0: ...}}, + {'subs': [{'k': 1, 'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'j': 3}]}]}, + ), + ( + {'subs': {'__all__': ..., 0: {'subsubs'}}}, + {'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'k': 2, 'subsubs': [{'i': 3, 'j': 3}]}]}, + ), + ], +) +def test_advanced_include_nested_lists(include, expected): class SubSubModel(BaseModel): i: int j: int @@ -586,65 +667,9 @@ class SubModel(BaseModel): class Model(BaseModel): subs: List[SubModel] - m = Model( - subs=[ - SubModel(k=1, subsubs=[SubSubModel(i=1, j=1), SubSubModel(i=2, j=2)]), - SubModel(k=2, subsubs=[SubSubModel(i=3, j=3)]), - ] - ) + m = Model(subs=[dict(k=1, subsubs=[dict(i=1, j=1), dict(i=2, j=2)]), dict(k=2, subsubs=[dict(i=3, j=3)])]) - # Normal nested __all__ - assert m.dict(include={'subs': {'__all__': {'subsubs': {'__all__': {'i'}}}}}) == { - 'subs': [{'subsubs': [{'i': 1}, {'i': 2}]}, {'subsubs': [{'i': 3}]}] - } - # Merge sub dicts - assert m.dict( - include={'subs': {'__all__': {'subsubs': {'__all__': {'i'}}}, 0: {'subsubs': {'__all__': {'j'}}}}} - ) == {'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3}]}]} - assert m.dict(include={'subs': {'__all__': {'subsubs': ...}, 0: {'subsubs': {'__all__': {'j'}}}}}) == { - 'subs': [{'subsubs': [{'j': 1}, {'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}] - } - assert m.dict(include={'subs': {'__all__': {'subsubs': {'__all__': {'j'}}}, 0: {'subsubs': ...}}}) == { - 'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'j': 3}]}] - } - # Merge sub sets - assert m.dict(include={'subs': {'__all__': {'subsubs': {0}}, 0: {'subsubs': {1}}}}) == { - 'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}] - } - # Merge sub dict-set - assert m.dict(include={'subs': {'__all__': {'subsubs': {0: {'i'}}}, 0: {'subsubs': {1}}}}) == { - 'subs': [{'subsubs': [{'i': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3}]}] - } - # Different keys - assert m.dict(include={'subs': {'__all__': {'subsubs'}, 0: {'k'}}}) == { - 'subs': [{'k': 1, 'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}] - } - assert m.dict(include={'subs': {'__all__': {'subsubs': ...}, 0: {'k'}}}) == { - 'subs': [{'k': 1, 'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}] - } - assert m.dict(include={'subs': {'__all__': {'subsubs'}, 0: {'k': ...}}}) == { - 'subs': [{'k': 1, 'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}] - } - # Nested different keys - assert m.dict(include={'subs': {'__all__': {'subsubs': {'__all__': {'i'}, 0: {'j'}}}}}) == { - 'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}] - } - assert m.dict(include={'subs': {'__all__': {'subsubs': {'__all__': {'i': ...}, 0: {'j'}}}}}) == { - 'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}] - } - assert m.dict(include={'subs': {'__all__': {'subsubs': {'__all__': {'i'}, 0: {'j': ...}}}}}) == { - 'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}] - } - # Ignore __all__ for index with defined include. - assert m.dict(include={'subs': {'__all__': {'subsubs'}, 0: {'subsubs': {'__all__': {'j'}}}}}) == { - 'subs': [{'subsubs': [{'j': 1}, {'j': 2}]}, {'subsubs': [{'i': 3, 'j': 3}]}] - } - assert m.dict(include={'subs': {'__all__': {'subsubs': {'__all__': {'j'}}}, 0: ...}}) == { - 'subs': [{'k': 1, 'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'subsubs': [{'j': 3}]}] - } - assert m.dict(include={'subs': {'__all__': ..., 0: {'subsubs'}}}) == { - 'subs': [{'subsubs': [{'i': 1, 'j': 1}, {'i': 2, 'j': 2}]}, {'k': 2, 'subsubs': [{'i': 3, 'j': 3}]}] - } + assert m.dict(include=include) == expected def test_field_set_ignore_extra():