Skip to content

Commit

Permalink
Merge 34527a1 into bb722cf
Browse files Browse the repository at this point in the history
  • Loading branch information
elacuesta authored Aug 26, 2021
2 parents bb722cf + 34527a1 commit 8f75e5a
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 287 deletions.
16 changes: 2 additions & 14 deletions itemadapter/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
_is_attrs_class,
_is_dataclass,
_is_pydantic_model,
is_attrs_instance,
is_dataclass_instance,
is_pydantic_instance,
is_scrapy_item,
)


Expand Down Expand Up @@ -111,7 +107,7 @@ def __init__(self, item: Any) -> None:

@classmethod
def is_item(cls, item: Any) -> bool:
return is_attrs_instance(item)
return _is_attrs_class(item) and not isinstance(item, type)

@classmethod
def is_item_class(cls, item_class: type) -> bool:
Expand All @@ -137,7 +133,7 @@ def __init__(self, item: Any) -> None:

@classmethod
def is_item(cls, item: Any) -> bool:
return is_dataclass_instance(item)
return _is_dataclass(item) and not isinstance(item, type)

@classmethod
def is_item_class(cls, item_class: type) -> bool:
Expand All @@ -157,10 +153,6 @@ class PydanticAdapter(AdapterInterface):

item: Any

@classmethod
def is_item(cls, item: Any) -> bool:
return is_pydantic_instance(item)

@classmethod
def is_item_class(cls, item_class: type) -> bool:
return _is_pydantic_model(item_class)
Expand Down Expand Up @@ -233,10 +225,6 @@ def field_names(self) -> KeysView:


class ScrapyItemAdapter(_MixinDictScrapyItemAdapter, AdapterInterface):
@classmethod
def is_item(cls, item: Any) -> bool:
return is_scrapy_item(item)

@classmethod
def is_item_class(cls, item_class: type) -> bool:
return issubclass(item_class, _get_scrapy_item_classes())
Expand Down
96 changes: 59 additions & 37 deletions itemadapter/utils.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import warnings

from types import MappingProxyType
from typing import Any


__all__ = ["is_item", "get_field_meta_from_class"]


def _get_scrapy_item_classes() -> tuple:
try:
import scrapy
except ImportError:
return ()
else:
try:
_base_item_cls = getattr(scrapy.item, "_BaseItem", scrapy.item.BaseItem) # deprecated
# handle deprecated base classes
_base_item_cls = getattr(scrapy.item, "_BaseItem", scrapy.item.BaseItem)
return (scrapy.item.Item, _base_item_cls)
except AttributeError:
return (scrapy.item.Item,)


def _is_dataclass(obj: Any) -> bool:
"""In py36, this returns False if the "dataclasses" backport module is not installed."""
try:
import dataclasses
except ImportError:
Expand Down Expand Up @@ -69,42 +76,6 @@ def _get_pydantic_model_metadata(item_model: Any, field_name: str) -> MappingPro
return MappingProxyType(metadata)


def is_dataclass_instance(obj: Any) -> bool:
"""Return True if the given object is a dataclass object, False otherwise.
In py36, this function returns False if the "dataclasses" backport is not available.
Taken from https://docs.python.org/3/library/dataclasses.html#dataclasses.is_dataclass.
"""
return _is_dataclass(obj) and not isinstance(obj, type)


def is_pydantic_instance(obj: Any) -> bool:
"""Return True if the given object is a Pydantic model, False otherwise."""
return _is_pydantic_model(type(obj)) and not isinstance(obj, type)


def is_attrs_instance(obj: Any) -> bool:
"""Return True if the given object is a attrs-based object, False otherwise."""
return _is_attrs_class(obj) and not isinstance(obj, type)


def is_scrapy_item(obj: Any) -> bool:
"""Return True if the given object is a Scrapy item, False otherwise."""
try:
import scrapy
except ImportError:
return False
if isinstance(obj, scrapy.item.Item):
return True
try:
# handle deprecated BaseItem
BaseItem = getattr(scrapy.item, "_BaseItem", scrapy.item.BaseItem)
return isinstance(obj, BaseItem)
except AttributeError:
return False


def is_item(obj: Any) -> bool:
"""Return True if the given object belongs to one of the supported types, False otherwise.
Expand Down Expand Up @@ -133,3 +104,54 @@ def get_field_meta_from_class(item_class: type, field_name: str) -> MappingProxy
from itemadapter.adapter import ItemAdapter

return ItemAdapter.get_field_meta_from_class(item_class, field_name)


# deprecated


def is_dataclass_instance(obj: Any) -> bool:
warnings.warn(
"itemadapter.utils.is_dataclass_instance is deprecated"
" and it will be removed in a future version",
category=DeprecationWarning,
stacklevel=2,
)
from itemadapter.adapter import DataclassAdapter

return DataclassAdapter.is_item(obj)


def is_attrs_instance(obj: Any) -> bool:
warnings.warn(
"itemadapter.utils.is_attrs_instance is deprecated"
" and it will be removed in a future version",
category=DeprecationWarning,
stacklevel=2,
)
from itemadapter.adapter import AttrsAdapter

return AttrsAdapter.is_item(obj)


def is_pydantic_instance(obj: Any) -> bool:
warnings.warn(
"itemadapter.utils.is_pydantic_instance is deprecated"
" and it will be removed in a future version",
category=DeprecationWarning,
stacklevel=2,
)
from itemadapter.adapter import PydanticAdapter

return PydanticAdapter.is_item(obj)


def is_scrapy_item(obj: Any) -> bool:
warnings.warn(
"itemadapter.utils.is_scrapy_item is deprecated"
" and it will be removed in a future version",
category=DeprecationWarning,
stacklevel=2,
)
from itemadapter.adapter import ScrapyItemAdapter

return ScrapyItemAdapter.is_item(obj)
10 changes: 9 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import importlib
from typing import Optional

from itemadapter.adapter import ItemAdapter
from itemadapter import ItemAdapter


def mocked_import(name, *args, **kwargs):
"""Allow only internal itemadapter imports."""
if name.split(".")[0] == "itemadapter":
return importlib.__import__(name, *args, **kwargs)
raise ImportError(name)


try:
Expand Down
69 changes: 69 additions & 0 deletions tests/test_adapter_attrs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import unittest
import warnings
from types import MappingProxyType
from unittest import mock

from itemadapter.adapter import AttrsAdapter
from itemadapter.utils import get_field_meta_from_class

from tests import (
AttrsItem,
DataClassItem,
PydanticModel,
ScrapyItem,
ScrapySubclassedItem,
mocked_import,
)


class AttrsTestCase(unittest.TestCase):
def test_false(self):
self.assertFalse(AttrsAdapter.is_item(int))
self.assertFalse(AttrsAdapter.is_item(sum))
self.assertFalse(AttrsAdapter.is_item(1234))
self.assertFalse(AttrsAdapter.is_item(object()))
self.assertFalse(AttrsAdapter.is_item(ScrapyItem()))
self.assertFalse(AttrsAdapter.is_item(DataClassItem()))
self.assertFalse(AttrsAdapter.is_item(PydanticModel()))
self.assertFalse(AttrsAdapter.is_item(ScrapySubclassedItem()))
self.assertFalse(AttrsAdapter.is_item("a string"))
self.assertFalse(AttrsAdapter.is_item(b"some bytes"))
self.assertFalse(AttrsAdapter.is_item({"a": "dict"}))
self.assertFalse(AttrsAdapter.is_item(["a", "list"]))
self.assertFalse(AttrsAdapter.is_item(("a", "tuple")))
self.assertFalse(AttrsAdapter.is_item({"a", "set"}))
self.assertFalse(AttrsAdapter.is_item(AttrsItem))

@unittest.skipIf(not AttrsItem, "attrs module is not available")
@mock.patch("builtins.__import__", mocked_import)
def test_module_not_available(self):
self.assertFalse(AttrsAdapter.is_item(AttrsItem(name="asdf", value=1234)))
with self.assertRaises(TypeError, msg="AttrsItem is not a valid item class"):
get_field_meta_from_class(AttrsItem, "name")

@unittest.skipIf(not AttrsItem, "attrs module is not available")
def test_true(self):
self.assertTrue(AttrsAdapter.is_item(AttrsItem()))
self.assertTrue(AttrsAdapter.is_item(AttrsItem(name="asdf", value=1234)))
# field metadata
self.assertEqual(
get_field_meta_from_class(AttrsItem, "name"), MappingProxyType({"serializer": str})
)
self.assertEqual(
get_field_meta_from_class(AttrsItem, "value"), MappingProxyType({"serializer": int})
)
with self.assertRaises(KeyError, msg="AttrsItem does not support field: non_existent"):
get_field_meta_from_class(AttrsItem, "non_existent")

def test_deprecated_is_instance(self):
from itemadapter.utils import is_attrs_instance

with warnings.catch_warnings(record=True) as caught:
is_attrs_instance(1)
self.assertEqual(len(caught), 1)
self.assertTrue(issubclass(caught[0].category, DeprecationWarning))
self.assertEqual(
"itemadapter.utils.is_attrs_instance is deprecated"
" and it will be removed in a future version",
str(caught[0].message),
)
70 changes: 70 additions & 0 deletions tests/test_adapter_dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import unittest
import warnings
from types import MappingProxyType
from unittest import mock

from itemadapter.adapter import DataclassAdapter
from itemadapter.utils import get_field_meta_from_class

from tests import (
AttrsItem,
DataClassItem,
PydanticModel,
ScrapyItem,
ScrapySubclassedItem,
mocked_import,
)


class DataclassTestCase(unittest.TestCase):
def test_false(self):
self.assertFalse(DataclassAdapter.is_item(int))
self.assertFalse(DataclassAdapter.is_item(sum))
self.assertFalse(DataclassAdapter.is_item(1234))
self.assertFalse(DataclassAdapter.is_item(object()))
self.assertFalse(DataclassAdapter.is_item(ScrapyItem()))
self.assertFalse(DataclassAdapter.is_item(AttrsItem()))
self.assertFalse(DataclassAdapter.is_item(PydanticModel()))
self.assertFalse(DataclassAdapter.is_item(ScrapySubclassedItem()))
self.assertFalse(DataclassAdapter.is_item("a string"))
self.assertFalse(DataclassAdapter.is_item(b"some bytes"))
self.assertFalse(DataclassAdapter.is_item({"a": "dict"}))
self.assertFalse(DataclassAdapter.is_item(["a", "list"]))
self.assertFalse(DataclassAdapter.is_item(("a", "tuple")))
self.assertFalse(DataclassAdapter.is_item({"a", "set"}))
self.assertFalse(DataclassAdapter.is_item(DataClassItem))

@unittest.skipIf(not DataClassItem, "dataclasses module is not available")
@mock.patch("builtins.__import__", mocked_import)
def test_module_not_available(self):
self.assertFalse(DataclassAdapter.is_item(DataClassItem(name="asdf", value=1234)))
with self.assertRaises(TypeError, msg="DataClassItem is not a valid item class"):
get_field_meta_from_class(DataClassItem, "name")

@unittest.skipIf(not DataClassItem, "dataclasses module is not available")
def test_true(self):
self.assertTrue(DataclassAdapter.is_item(DataClassItem()))
self.assertTrue(DataclassAdapter.is_item(DataClassItem(name="asdf", value=1234)))
# field metadata
self.assertEqual(
get_field_meta_from_class(DataClassItem, "name"), MappingProxyType({"serializer": str})
)
self.assertEqual(
get_field_meta_from_class(DataClassItem, "value"),
MappingProxyType({"serializer": int}),
)
with self.assertRaises(KeyError, msg="DataClassItem does not support field: non_existent"):
get_field_meta_from_class(DataClassItem, "non_existent")

def test_deprecated_is_instance(self):
from itemadapter.utils import is_dataclass_instance

with warnings.catch_warnings(record=True) as caught:
is_dataclass_instance(1)
self.assertEqual(len(caught), 1)
self.assertTrue(issubclass(caught[0].category, DeprecationWarning))
self.assertEqual(
"itemadapter.utils.is_dataclass_instance is deprecated"
" and it will be removed in a future version",
str(caught[0].message),
)
Loading

0 comments on commit 8f75e5a

Please sign in to comment.