Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate undocumented predicates from the utils module #56

Merged
merged 17 commits into from Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 2 additions & 14 deletions itemadapter/adapter.py
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
@@ -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
@@ -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
@@ -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
@@ -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),
)