-
Notifications
You must be signed in to change notification settings - Fork 92
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for nested fields in SPIDERMON_VALIDATION_ERRORS_FIELD (#417
) * Add support for nested fields in SPIDERMON_VALIDATION_ERRORS_FIELD * Added docs for this features * Improve docstrings
- Loading branch information
Showing
6 changed files
with
114 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from typing import Any, List | ||
from itemadapter import ItemAdapter | ||
|
||
|
||
def traverse_nested(obj: ItemAdapter, keys: List[str]) -> ItemAdapter: | ||
""" | ||
Get the last nested attribute from a list of keys within an ItemAdapter object. | ||
Raises: | ||
KeyError: if any of the keys in the path is not defined. | ||
""" | ||
current_obj = obj | ||
while keys: | ||
try: | ||
# Traverse next level of item object | ||
key = keys.pop(0) | ||
current_obj = ItemAdapter(current_obj[key]) | ||
except KeyError: | ||
raise KeyError(f'Invalid key "{key}" for {current_obj} in {obj}') | ||
|
||
return current_obj | ||
|
||
|
||
def get_nested_attribute(item: ItemAdapter, attribute_path: str): | ||
""" | ||
Get the value of a nested attribute within an ItemAdapter. | ||
Raises: | ||
KeyError: if any of the keys in the path is not defined. | ||
""" | ||
*keys, last_key = attribute_path.split(".") | ||
nested_obj = traverse_nested(item, keys) | ||
return nested_obj.get(last_key) | ||
|
||
|
||
def set_nested_attribute(item: ItemAdapter, attribute_path: str, value: Any): | ||
""" | ||
Set the value of a nested attribute within an ItemAdapter. | ||
Raises: | ||
KeyError: if any of the keys in the path is not defined or | ||
if the last key in the path is not supported by its parent field. | ||
""" | ||
*keys, last_key = attribute_path.split(".") | ||
nested_obj = traverse_nested(item, keys) | ||
if not isinstance(nested_obj, ItemAdapter): | ||
nested_obj = ItemAdapter(nested_obj) | ||
|
||
nested_obj[last_key] = value |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import pytest | ||
from dataclasses import dataclass | ||
|
||
from itemadapter import ItemAdapter | ||
from spidermon.contrib.utils.attributes import ( | ||
get_nested_attribute, | ||
set_nested_attribute, | ||
) | ||
|
||
|
||
def test_get_nested_attribute(): | ||
item = ItemAdapter({"foo": "bar", "attr1": {"attr2": {"attr3": "foobar"}}}) | ||
|
||
assert get_nested_attribute(item, "foo") == "bar" | ||
assert get_nested_attribute(item, "attr1.attr2.attr3") == "foobar" | ||
assert get_nested_attribute(item, "missing_attribute") is None | ||
|
||
# Missing intermiddle attribute | ||
with pytest.raises(KeyError): | ||
get_nested_attribute(item, "attr1.missing_attribute.attr2") | ||
|
||
|
||
def test_set_nested_attribute(): | ||
item = ItemAdapter({"foo": None, "attr1": {"attr2": {"attr3": None}}}) | ||
set_nested_attribute(item, "foo", "foobar") | ||
assert item["foo"] == "foobar" | ||
|
||
set_nested_attribute(item, "attr1.attr2.attr3", "bar") | ||
assert get_nested_attribute(item, "attr1.attr2.attr3") == "bar" | ||
|
||
# Set undefined attribute when underlaying class allows it | ||
set_nested_attribute(item, "missing_attribute", "foo") | ||
assert item["missing_attribute"] == "foo" | ||
|
||
# Set undefined attribute when underlaying class doesn't allow it | ||
@dataclass | ||
class NestedField: | ||
foo: str | ||
|
||
@dataclass | ||
class DummyItem: | ||
attr1: NestedField | ||
|
||
item = ItemAdapter(DummyItem(attr1=NestedField(foo="bar"))) | ||
with pytest.raises( | ||
KeyError, match="NestedField does not support field: missing_attribute" | ||
): | ||
set_nested_attribute(item, "attr1.missing_attribute", "foo") |