diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f3fa793..c874bf2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,7 +45,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} @@ -55,6 +55,35 @@ jobs: - name: Run tests run: tox -e py + - name: Upload coverage report + run: bash <(curl -s https://codecov.io/bash) -cF tests + + + tests-no-extra-deps: + name: "Test: py${{ matrix.python-version }}, Ubuntu, no extra deps" + runs-on: ubuntu-18.04 + strategy: + matrix: + python-version: [3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e no-extra-deps + + - name: Upload coverage report + run: bash <(curl -s https://codecov.io/bash) -cF tests + + - name: Upload coverage report run: bash <(curl -s https://codecov.io/bash) diff --git a/tests/__init__.py b/tests/__init__.py index eca561d..76f9ced 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,62 +1,50 @@ -from itemadapter.adapter import ItemAdapter +import os +import sys +from unittest import skipIf, TestCase as _TestCase try: import attr except ImportError: AttrsItem = None - AttrsItemNested = None AttrsItemWithoutInit = None else: + if os.environ.get("ITEMADAPTER_NO_EXTRA_DEPS"): + AttrsItem = None + AttrsItemWithoutInit = None + else: - @attr.s - class AttrsItem: - name = attr.ib(default=None, metadata={"serializer": str}) - value = attr.ib(default=None, metadata={"serializer": int}) + @attr.s + class AttrsItem: + name = attr.ib(default=None, metadata={"serializer": str}) + value = attr.ib(default=None, metadata={"serializer": int}) - @attr.s - class AttrsItemNested: - nested = attr.ib(type=AttrsItem) - adapter = attr.ib(type=ItemAdapter) - dict_ = attr.ib(type=dict) - list_ = attr.ib(type=list) - set_ = attr.ib(type=set) - tuple_ = attr.ib(type=tuple) - int_ = attr.ib(type=int) - - @attr.s(init=False) - class AttrsItemWithoutInit: - name = attr.ib(default=None, metadata={"serializer": str}) - value = attr.ib(default=None, metadata={"serializer": int}) + @attr.s(init=False) + class AttrsItemWithoutInit: + name = attr.ib(default=None, metadata={"serializer": str}) + value = attr.ib(default=None, metadata={"serializer": int}) try: from dataclasses import dataclass, field except ImportError: DataClassItem = None - DataClassItemNested = None DataClassWithoutInit = None else: + if os.environ.get("ITEMADAPTER_NO_EXTRA_DEPS") and (3, 6) <= sys.version_info < (3, 7): + DataClassItem = None + DataClassWithoutInit = None + else: - @dataclass - class DataClassItem: - name: str = field(default_factory=lambda: None, metadata={"serializer": str}) - value: int = field(default_factory=lambda: None, metadata={"serializer": int}) - - @dataclass - class DataClassItemNested: - nested: DataClassItem - adapter: ItemAdapter - dict_: dict - list_: list - set_: set - tuple_: tuple - int_: int + @dataclass + class DataClassItem: + name: str = field(default_factory=lambda: None, metadata={"serializer": str}) + value: int = field(default_factory=lambda: None, metadata={"serializer": int}) - @dataclass(init=False) - class DataClassWithoutInit: - name: str = field(metadata={"serializer": str}) - value: int = field(metadata={"serializer": int}) + @dataclass(init=False) + class DataClassWithoutInit: + name: str = field(metadata={"serializer": str}) + value: int = field(metadata={"serializer": int}) try: @@ -66,16 +54,90 @@ class DataClassWithoutInit: ScrapySubclassedItem = None ScrapySubclassedItemNested = None else: + if os.environ.get("ITEMADAPTER_NO_EXTRA_DEPS"): + ScrapyItem = None + ScrapySubclassedItem = None + ScrapySubclassedItemNested = None + else: + + class ScrapySubclassedItem(ScrapyItem): + name = Field(serializer=str) + value = Field(serializer=int) + + class ScrapySubclassedItemNested(ScrapyItem): + nested = Field() + adapter = Field() + dict_ = Field() + list_ = Field() + set_ = Field() + tuple_ = Field() + int_ = Field() + + +class ImportRaiser: + def __init__(self, *packages): + self.packages = set(packages) + + def find_spec(self, fullname, path, target=None): + if fullname in self.packages: + raise ImportError + + +class TestCase(_TestCase): + """Custom TestCase subclass which handles disabling installed extra + packages during tests when ITEMADAPTER_NO_EXTRA_DEPS is set, as well as + disabling test cases that require one or more unavailable extra + dependencies. + + This is needed to disable packages that cannot be uninstalled because + pytest depends on them. + """ + + _extra_modules = ("attr", "scrapy") + + def setUp(self): + super().setUp() + + required_extra_modules = getattr(self, "required_extra_modules", None) + if required_extra_modules: + requirement_map = { + "attr": AttrsItem, + "dataclasses": DataClassItem, + "scrapy": ScrapyItem, + } + unknown_extra_modules = [ + module for module in required_extra_modules if module not in requirement_map + ] + if unknown_extra_modules: + raise NotImplementedError( + "Unknown extra modules: {}".format(unknown_extra_modules) + ) + unavaliable_extra_modules = [ + module for module in required_extra_modules if not requirement_map[module] + ] + if unavaliable_extra_modules: + self.skipTest("cannot import; {}".format(", ".join(unavaliable_extra_modules))) + + self._removed_modules = {} + if os.environ.get("ITEMADAPTER_NO_EXTRA_DEPS"): + if (3, 6) <= sys.version_info < (3, 7): + self._extra_modules = self._extra_modules + ("dataclasses",) + sys.meta_path.insert(0, ImportRaiser(*self._extra_modules)) + for package in self._extra_modules: + if package in sys.modules: + self._removed_modules[package] = sys.modules[package] + del sys.modules[package] + + def tearDown(self): + super().tearDown() + + if os.environ.get("ITEMADAPTER_NO_EXTRA_DEPS"): + del sys.meta_path[0] + for package in self._extra_modules: + if package in self._removed_modules: + sys.modules[package] = self._removed_modules[package] + - class ScrapySubclassedItem(ScrapyItem): - name = Field(serializer=str) - value = Field(serializer=int) - - class ScrapySubclassedItemNested(ScrapyItem): - nested = Field() - adapter = Field() - dict_ = Field() - list_ = Field() - set_ = Field() - tuple_ = Field() - int_ = Field() +requires_attr = skipIf(not AttrsItem, "cannot import attr") +requires_dataclasses = skipIf(not DataClassItem, "cannot import dataclasses") +requires_scrapy = skipIf(not ScrapyItem, "cannot import scrapy") diff --git a/tests/attr_utils.py b/tests/attr_utils.py new file mode 100644 index 0000000..362627b --- /dev/null +++ b/tests/attr_utils.py @@ -0,0 +1,19 @@ +"""Code for attr.s tests that must only be imported from within tests because +it imports from itemadapter.""" + +import attr + +from itemadapter import ItemAdapter + +from tests import AttrsItem + + +@attr.s +class AttrsItemNested: + nested = attr.ib(type=AttrsItem) + adapter = attr.ib(type=ItemAdapter) + dict_ = attr.ib(type=dict) + list_ = attr.ib(type=list) + set_ = attr.ib(type=set) + tuple_ = attr.ib(type=tuple) + int_ = attr.ib(type=int) diff --git a/tests/dataclasses_utils.py b/tests/dataclasses_utils.py new file mode 100644 index 0000000..4dc983b --- /dev/null +++ b/tests/dataclasses_utils.py @@ -0,0 +1,19 @@ +"""Code for dataclass tests that must only be imported from within tests +because it imports from itemadapter.""" + +from dataclasses import dataclass + +from itemadapter import ItemAdapter + +from tests import DataClassItem + + +@dataclass +class DataClassItemNested: + nested: DataClassItem + adapter: ItemAdapter + dict_: dict + list_: list + set_: set + tuple_: tuple + int_: int diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index 671a10e..0000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -attrs -dataclasses; python_version == "3.6" -pytest-cov>=2.8 -pytest>=5.4 -scrapy>=2.0 diff --git a/tests/test_adapter.py b/tests/test_adapter.py index a3c8b28..bb6d941 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -1,37 +1,44 @@ import unittest +from importlib import import_module from types import MappingProxyType from typing import KeysView -from itemadapter.adapter import ItemAdapter - from tests import ( AttrsItem, - AttrsItemNested, AttrsItemWithoutInit, DataClassItem, - DataClassItemNested, DataClassWithoutInit, + requires_attr, + requires_dataclasses, + requires_scrapy, ScrapySubclassedItem, ScrapySubclassedItemNested, + TestCase, ) -class ItemAdapterReprTestCase(unittest.TestCase): +class ItemAdapterReprTestCase(TestCase): def test_repr_dict(self): + from itemadapter.adapter import ItemAdapter + item = dict(name="asdf", value=1234) adapter = ItemAdapter(item) self.assertEqual(repr(adapter), "") - @unittest.skipIf(not ScrapySubclassedItem, "scrapy module is not available") + @requires_scrapy def test_repr_scrapy_item(self): + from itemadapter.adapter import ItemAdapter + item = ScrapySubclassedItem(name="asdf", value=1234) adapter = ItemAdapter(item) self.assertEqual( repr(adapter), "" ) - @unittest.skipIf(not DataClassItem, "dataclasses module is not available") + @requires_dataclasses def test_repr_dataclass(self): + from itemadapter.adapter import ItemAdapter + item = DataClassItem(name="asdf", value=1234) adapter = ItemAdapter(item) self.assertEqual( @@ -39,8 +46,10 @@ def test_repr_dataclass(self): "", ) - @unittest.skipIf(not DataClassWithoutInit, "dataclasses module is not available") + @requires_dataclasses def test_repr_dataclass_init_false(self): + from itemadapter.adapter import ItemAdapter + item = DataClassWithoutInit() adapter = ItemAdapter(item) self.assertEqual(repr(adapter), "") @@ -49,8 +58,10 @@ def test_repr_dataclass_init_false(self): repr(adapter), "" ) - @unittest.skipIf(not AttrsItem, "attrs module is not available") + @requires_attr def test_repr_attrs(self): + from itemadapter.adapter import ItemAdapter + item = AttrsItem(name="asdf", value=1234) adapter = ItemAdapter(item) self.assertEqual( @@ -58,8 +69,10 @@ def test_repr_attrs(self): "", ) - @unittest.skipIf(not AttrsItemWithoutInit, "attrs module is not available") + @requires_attr def test_repr_attrs_init_false(self): + from itemadapter.adapter import ItemAdapter + item = AttrsItemWithoutInit() adapter = ItemAdapter(item) self.assertEqual(repr(adapter), "") @@ -69,8 +82,10 @@ def test_repr_attrs_init_false(self): ) -class ItemAdapterInitError(unittest.TestCase): +class ItemAdapterInitError(TestCase): def test_non_item(self): + from itemadapter.adapter import ItemAdapter + with self.assertRaises(TypeError): ItemAdapter(ScrapySubclassedItem) with self.assertRaises(TypeError): @@ -82,13 +97,22 @@ def test_non_item(self): class BaseTestMixin: item_class = None - item_class_nested = None + item_class_nested_path = None def setUp(self): + super().setUp() if self.item_class is None: raise unittest.SkipTest() + @property + def item_class_nested(self): + module_path, class_name = self.item_class_nested_path.rsplit(".", maxsplit=1) + module = import_module(module_path) + return getattr(module, class_name) + def test_get_set_value(self): + from itemadapter.adapter import ItemAdapter + item = self.item_class() adapter = ItemAdapter(item) self.assertEqual(adapter.get("name"), None) @@ -108,17 +132,23 @@ def test_get_set_value(self): self.assertEqual(adapter["value"], 1234) def test_get_value_keyerror(self): + from itemadapter.adapter import ItemAdapter + item = self.item_class() adapter = ItemAdapter(item) with self.assertRaises(KeyError): adapter["undefined_field"] def test_as_dict(self): + from itemadapter.adapter import ItemAdapter + item = self.item_class(name="asdf", value=1234) adapter = ItemAdapter(item) self.assertEqual(dict(name="asdf", value=1234), dict(adapter)) def test_as_dict_nested(self): + from itemadapter.adapter import ItemAdapter + item = self.item_class_nested( nested=self.item_class(name="asdf", value=1234), adapter=ItemAdapter(dict(foo="bar", nested_list=[1, 2, 3, 4, 5])), @@ -143,6 +173,8 @@ def test_as_dict_nested(self): ) def test_field_names(self): + from itemadapter.adapter import ItemAdapter + item = self.item_class(name="asdf", value=1234) adapter = ItemAdapter(item) self.assertIsInstance(adapter.field_names(), KeysView) @@ -151,12 +183,16 @@ def test_field_names(self): class NonDictTestMixin(BaseTestMixin): def test_set_value_keyerror(self): + from itemadapter.adapter import ItemAdapter + item = self.item_class() adapter = ItemAdapter(item) with self.assertRaises(KeyError): adapter["undefined_field"] = "some value" def test_metadata_common(self): + from itemadapter.adapter import ItemAdapter + adapter = ItemAdapter(self.item_class()) self.assertIsInstance(adapter.get_field_meta("name"), MappingProxyType) self.assertIsInstance(adapter.get_field_meta("value"), MappingProxyType) @@ -164,11 +200,15 @@ def test_metadata_common(self): adapter.get_field_meta("undefined_field") def test_get_field_meta_defined_fields(self): + from itemadapter.adapter import ItemAdapter + adapter = ItemAdapter(self.item_class()) self.assertEqual(adapter.get_field_meta("name"), MappingProxyType({"serializer": str})) self.assertEqual(adapter.get_field_meta("value"), MappingProxyType({"serializer": int})) def test_delitem_len_iter(self): + from itemadapter.adapter import ItemAdapter + item = self.item_class(name="asdf", value=1234) adapter = ItemAdapter(item) self.assertEqual(len(adapter), 2) @@ -190,23 +230,29 @@ def test_delitem_len_iter(self): del adapter["undefined_field"] -class DictTestCase(unittest.TestCase, BaseTestMixin): +class DictTestCase(TestCase, BaseTestMixin): item_class = dict item_class_nested = dict def test_get_value_keyerror_item_dict(self): """Instantiate without default values""" + from itemadapter.adapter import ItemAdapter + adapter = ItemAdapter(self.item_class()) with self.assertRaises(KeyError): adapter["name"] def test_empty_metadata(self): + from itemadapter.adapter import ItemAdapter + adapter = ItemAdapter(self.item_class(name="foo", value=5)) for field_name in ("name", "value", "undefined_field"): self.assertEqual(adapter.get_field_meta(field_name), MappingProxyType({})) def test_field_names_updated(self): + from itemadapter.adapter import ItemAdapter + item = self.item_class(name="asdf") field_names = ItemAdapter(item).field_names() self.assertEqual(sorted(field_names), ["name"]) @@ -214,25 +260,27 @@ def test_field_names_updated(self): self.assertEqual(sorted(field_names), ["name", "value"]) -class ScrapySubclassedItemTestCase(NonDictTestMixin, unittest.TestCase): +class ScrapySubclassedItemTestCase(NonDictTestMixin, TestCase): item_class = ScrapySubclassedItem item_class_nested = ScrapySubclassedItemNested def test_get_value_keyerror_item_dict(self): """Instantiate without default values""" + from itemadapter.adapter import ItemAdapter + adapter = ItemAdapter(self.item_class()) with self.assertRaises(KeyError): adapter["name"] -class DataClassItemTestCase(NonDictTestMixin, unittest.TestCase): +class DataClassItemTestCase(NonDictTestMixin, TestCase): item_class = DataClassItem - item_class_nested = DataClassItemNested + item_class_nested_path = "tests.dataclasses_utils.DataClassItemNested" -class AttrsItemTestCase(NonDictTestMixin, unittest.TestCase): +class AttrsItemTestCase(NonDictTestMixin, TestCase): item_class = AttrsItem - item_class_nested = AttrsItemNested + item_class_nested_path = "tests.attr_utils.AttrsItemNested" diff --git a/tests/test_utils.py b/tests/test_utils.py index 1ca1be2..41c5580 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,16 +1,24 @@ import unittest -from unittest import mock from types import MappingProxyType +from unittest import mock from itemadapter.utils import ( get_field_meta_from_class, is_attrs_instance, is_dataclass_instance, - is_item, is_scrapy_item, ) -from tests import AttrsItem, DataClassItem, ScrapyItem, ScrapySubclassedItem +from tests import ( + AttrsItem, + DataClassItem, + requires_attr, + requires_dataclasses, + requires_scrapy, + ScrapyItem, + ScrapySubclassedItem, + TestCase, +) def mocked_import(name, *args, **kwargs): @@ -34,6 +42,8 @@ class DictSubclass(dict): class ItemLikeTestCase(unittest.TestCase): def test_false(self): + from itemadapter.utils import is_item + self.assertFalse(is_item(int)) self.assertFalse(is_item(sum)) self.assertFalse(is_item(1234)) @@ -50,30 +60,38 @@ def test_false(self): self.assertFalse(is_item(AttrsItem)) def test_true_dict(self): + from itemadapter.utils import is_item + self.assertTrue(is_item({"a": "dict"})) - @unittest.skipIf(not ScrapySubclassedItem, "scrapy module is not available") + @requires_scrapy def test_true_scrapy(self): + from itemadapter.utils import is_item + self.assertTrue(is_item(ScrapyItem())) self.assertTrue(is_item(ScrapySubclassedItem(name="asdf", value=1234))) - @unittest.skipIf(not DataClassItem, "dataclasses module is not available") + @requires_dataclasses def test_true_dataclass(self): + from itemadapter.utils import is_item + self.assertTrue(is_item(DataClassItem(name="asdf", value=1234))) - @unittest.skipIf(not AttrsItem, "attrs module is not available") + @requires_attr def test_true_attrs(self): + from itemadapter.utils import is_item + self.assertTrue(is_item(AttrsItem(name="asdf", value=1234))) -class AttrsTestCase(unittest.TestCase): +class AttrsTestCase(TestCase): def test_false(self): + from itemadapter.utils import is_attrs_instance + self.assertFalse(is_attrs_instance(int)) self.assertFalse(is_attrs_instance(sum)) self.assertFalse(is_attrs_instance(1234)) self.assertFalse(is_attrs_instance(object())) - self.assertFalse(is_attrs_instance(ScrapyItem())) - self.assertFalse(is_attrs_instance(ScrapySubclassedItem())) self.assertFalse(is_attrs_instance("a string")) self.assertFalse(is_attrs_instance(b"some bytes")) self.assertFalse(is_attrs_instance({"a": "dict"})) @@ -82,15 +100,17 @@ def test_false(self): self.assertFalse(is_attrs_instance({"a", "set"})) self.assertFalse(is_attrs_instance(AttrsItem)) - @unittest.skipIf(not AttrsItem, "attrs module is not available") + @requires_attr @mock.patch("builtins.__import__", mocked_import) def test_module_not_available(self): self.assertFalse(is_attrs_instance(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") + @requires_attr def test_true(self): + from itemadapter.utils import is_attrs_instance + self.assertTrue(is_attrs_instance(AttrsItem())) self.assertTrue(is_attrs_instance(AttrsItem(name="asdf", value=1234))) # field metadata @@ -104,15 +124,14 @@ def test_true(self): get_field_meta_from_class(AttrsItem, "non_existent") -class DataclassTestCase(unittest.TestCase): +class DataclassTestCase(TestCase): def test_false(self): + from itemadapter.utils import is_dataclass_instance + self.assertFalse(is_dataclass_instance(int)) self.assertFalse(is_dataclass_instance(sum)) self.assertFalse(is_dataclass_instance(1234)) self.assertFalse(is_dataclass_instance(object())) - self.assertFalse(is_dataclass_instance(ScrapyItem())) - self.assertFalse(is_dataclass_instance(AttrsItem())) - self.assertFalse(is_dataclass_instance(ScrapySubclassedItem())) self.assertFalse(is_dataclass_instance("a string")) self.assertFalse(is_dataclass_instance(b"some bytes")) self.assertFalse(is_dataclass_instance({"a": "dict"})) @@ -121,15 +140,17 @@ def test_false(self): self.assertFalse(is_dataclass_instance({"a", "set"})) self.assertFalse(is_dataclass_instance(DataClassItem)) - @unittest.skipIf(not DataClassItem, "dataclasses module is not available") + @requires_dataclasses @mock.patch("builtins.__import__", mocked_import) def test_module_not_available(self): self.assertFalse(is_dataclass_instance(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") + @requires_dataclasses def test_true(self): + from itemadapter.utils import is_dataclass_instance + self.assertTrue(is_dataclass_instance(DataClassItem())) self.assertTrue(is_dataclass_instance(DataClassItem(name="asdf", value=1234))) # field metadata @@ -144,13 +165,14 @@ def test_true(self): get_field_meta_from_class(DataClassItem, "non_existent") -class ScrapyItemTestCase(unittest.TestCase): +class ScrapyItemTestCase(TestCase): def test_false(self): + from itemadapter.utils import is_scrapy_item + self.assertFalse(is_scrapy_item(int)) self.assertFalse(is_scrapy_item(sum)) self.assertFalse(is_scrapy_item(1234)) self.assertFalse(is_scrapy_item(object())) - self.assertFalse(is_scrapy_item(AttrsItem())) self.assertFalse(is_scrapy_item("a string")) self.assertFalse(is_scrapy_item(b"some bytes")) self.assertFalse(is_scrapy_item({"a": "dict"})) @@ -159,15 +181,17 @@ def test_false(self): self.assertFalse(is_scrapy_item({"a", "set"})) self.assertFalse(is_scrapy_item(ScrapySubclassedItem)) - @unittest.skipIf(not ScrapySubclassedItem, "scrapy module is not available") + @requires_scrapy @mock.patch("builtins.__import__", mocked_import) def test_module_not_available(self): self.assertFalse(is_scrapy_item(ScrapySubclassedItem(name="asdf", value=1234))) with self.assertRaises(TypeError, msg="ScrapySubclassedItem is not a valid item class"): get_field_meta_from_class(ScrapySubclassedItem, "name") - @unittest.skipIf(not ScrapySubclassedItem, "scrapy module is not available") + @requires_scrapy def test_true(self): + from itemadapter.utils import is_scrapy_item + self.assertTrue(is_scrapy_item(ScrapyItem())) self.assertTrue(is_scrapy_item(ScrapySubclassedItem())) self.assertTrue(is_scrapy_item(ScrapySubclassedItem(name="asdf", value=1234))) @@ -188,16 +212,21 @@ def test_true(self): scrapy = None -class ScrapyDeprecatedBaseItemTestCase(unittest.TestCase): +class ScrapyDeprecatedBaseItemTestCase(TestCase): """ - Tests for deprecated classes. These will go away once the upstream classes are removed. + Tests for deprecated classes. These will go away once the upstream classes + are removed. """ + required_extra_modules = ("scrapy",) + @unittest.skipIf( - scrapy is None or not hasattr(scrapy.item, "_BaseItem"), + not hasattr(scrapy.item, "_BaseItem"), "scrapy.item._BaseItem not available", ) def test_deprecated_underscore_baseitem(self): + from itemadapter.utils import is_scrapy_item + class SubClassed_BaseItem(scrapy.item._BaseItem): pass @@ -205,21 +234,23 @@ class SubClassed_BaseItem(scrapy.item._BaseItem): self.assertTrue(is_scrapy_item(SubClassed_BaseItem())) @unittest.skipIf( - scrapy is None or not hasattr(scrapy.item, "BaseItem"), + not hasattr(scrapy.item, "BaseItem"), "scrapy.item.BaseItem not available", ) def test_deprecated_baseitem(self): + from itemadapter.utils import is_scrapy_item + class SubClassedBaseItem(scrapy.item.BaseItem): pass self.assertTrue(is_scrapy_item(scrapy.item.BaseItem())) self.assertTrue(is_scrapy_item(SubClassedBaseItem())) - @unittest.skipIf(scrapy is None, "scrapy module is not available") def test_removed_baseitem(self): """ Mock the scrapy.item module so it does not contain the deprecated _BaseItem class """ + from itemadapter.utils import is_scrapy_item class MockItemModule: Item = ScrapyItem diff --git a/tox.ini b/tox.ini index be454d7..79af4ce 100644 --- a/tox.ini +++ b/tox.ini @@ -1,36 +1,46 @@ [tox] -envlist = bandit,flake8,typing,black,py +envlist = black,bandit,flake8,typing,py,no-extra-deps + +[base] +deps = + pytest>=5.4 + pytest-cov>=2.8 [testenv] deps = - -rtests/requirements.txt + {[base]deps} + attrs + dataclasses; python_version >= '3.6' and python_version < '3.7' + scrapy>=2.0 commands = - pytest --verbose --cov=itemadapter --cov-report=term-missing --cov-report=html --cov-report=xml --doctest-glob=README.md {posargs: itemadapter README.md tests} + pytest --verbose --cov=itemadapter --cov-report=term-missing --cov-report=html --cov-report=xml --cov-append --doctest-glob=README.md {posargs:itemadapter README.md tests} [testenv:bandit] -basepython = python3 deps = bandit commands = bandit -r {posargs:itemadapter} +[testenv:black] +deps = + black>=19.10b0 +commands = + black --check {posargs:itemadapter tests} + [testenv:flake8] -basepython = python3 deps = flake8>=3.7.9 commands = flake8 --exclude=.git,.tox,venv* {posargs:itemadapter tests} +[testenv:no-extra-deps] +deps = + {[testenv]deps} +setenv = + ITEMADAPTER_NO_EXTRA_DEPS=True + [testenv:typing] -basepython = python3 deps = mypy==0.770 commands = mypy --ignore-missing-imports --follow-imports=skip {posargs:itemadapter} - -[testenv:black] -basepython = python3 -deps = - black>=19.10b0 -commands = - black --check {posargs:itemadapter tests}