Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-itemadapter for openSUSE:Factory checked in at 2023-12-08 22:32:13 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-itemadapter (Old) and /work/SRC/openSUSE:Factory/.python-itemadapter.new.25432 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-itemadapter" Fri Dec 8 22:32:13 2023 rev:4 rq:1131744 version:0.8.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-itemadapter/python-itemadapter.changes 2022-09-29 18:15:18.347484050 +0200 +++ /work/SRC/openSUSE:Factory/.python-itemadapter.new.25432/python-itemadapter.changes 2023-12-08 22:32:44.461517866 +0100 @@ -1,0 +2,12 @@ +Thu Dec 7 22:47:54 UTC 2023 - Dirk Müller <[email protected]> + +- update to 0.8.0: + * Dropped Python 3.6 support, and made Python 3.11 support + official + * It is now possible to declare custom `ItemAdapter` subclasses + with their own `ADAPTER_CLASSES` attribute, allowing to + support different item types in different parts of the same + code base + * Improved type hint support + +------------------------------------------------------------------- Old: ---- itemadapter-0.7.0.tar.gz New: ---- itemadapter-0.8.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-itemadapter.spec ++++++ --- /var/tmp/diff_new_pack.JmX37s/_old 2023-12-08 22:32:45.429553485 +0100 +++ /var/tmp/diff_new_pack.JmX37s/_new 2023-12-08 22:32:45.433553631 +0100 @@ -1,7 +1,7 @@ # # spec file # -# Copyright (c) 2022 SUSE LLC +# Copyright (c) 2023 SUSE LLC # Copyright (c) 2016, Martin Hauke <[email protected]> # # All modifications and additions to the file contributed by third parties @@ -30,7 +30,7 @@ # Scrapy on TW has disabled python36 due to uvloop %define skip_python36 1 Name: python-itemadapter%{psuffix} -Version: 0.7.0 +Version: 0.8.0 Release: 0 Summary: Wrapper for data container objects License: BSD-3-Clause ++++++ itemadapter-0.7.0.tar.gz -> itemadapter-0.8.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/.bumpversion.cfg new/itemadapter-0.8.0/.bumpversion.cfg --- old/itemadapter-0.7.0/.bumpversion.cfg 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/.bumpversion.cfg 2023-03-30 21:09:01.000000000 +0200 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.7.0 +current_version = 0.8.0 commit = True tag = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/.github/workflows/tests.yml new/itemadapter-0.8.0/.github/workflows/tests.yml --- old/itemadapter-0.7.0/.github/workflows/tests.yml 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/.github/workflows/tests.yml 2023-03-30 21:09:01.000000000 +0200 @@ -8,7 +8,7 @@ runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/Changelog.md new/itemadapter-0.8.0/Changelog.md --- old/itemadapter-0.7.0/Changelog.md 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/Changelog.md 2023-03-30 21:09:01.000000000 +0200 @@ -1,5 +1,21 @@ # Changelog +### 0.8.0 (2023-03-30) + +Dropped Python 3.6 support, and made Python 3.11 support official +([#65](https://github.com/scrapy/itemadapter/pull/65), +[#66](https://github.com/scrapy/itemadapter/pull/66), +[#69](https://github.com/scrapy/itemadapter/pull/69)). + +It is now possible to declare custom `ItemAdapter` subclasses with their own +`ADAPTER_CLASSES` attribute, allowing to support different item types in +different parts of the same code base +([#68](https://github.com/scrapy/itemadapter/pull/68)). + +Improved type hint support +([#67](https://github.com/scrapy/itemadapter/pull/67)). + + ### 0.7.0 (2022-08-02) ItemAdapter.get_field_names_from_class diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/README.md new/itemadapter-0.8.0/README.md --- old/itemadapter-0.7.0/README.md 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/README.md 2023-03-30 21:09:01.000000000 +0200 @@ -24,11 +24,8 @@ ## Requirements -* Python 3.6+ +* Python 3.7+ * [`scrapy`](https://scrapy.org/): optional, needed to interact with `scrapy` items -* `dataclasses` ([stdlib](https://docs.python.org/3/library/dataclasses.html) in Python 3.7+, - or its [backport](https://pypi.org/project/dataclasses/) in Python 3.6): optional, needed - to interact with `dataclass`-based items * [`attrs`](https://pypi.org/project/attrs/): optional, needed to interact with `attrs`-based items * [`pydantic`](https://pypi.org/project/pydantic/): optional, needed to interact with `pydantic`-based items @@ -377,6 +374,46 @@ >>> ``` +### Multiple adapter classes + +If you need to have different handlers and/or priorities for different cases +you can subclass the `ItemAdapter` class and set the `ADAPTER_CLASSES` +attribute as needed: + + +**Example** +```python +>>> from collections import deque +>>> from itemadapter.adapter import ( +... ItemAdapter, +... AttrsAdapter, +... DataclassAdapter, +... DictAdapter, +... PydanticAdapter, +... ScrapyItemAdapter, +... ) +>>> from scrapy.item import Item, Field +>>> +>>> class BuiltinTypesItemAdapter(ItemAdapter): +... ADAPTER_CLASSES = deque([DictAdapter, DataclassAdapter]) +... +>>> class ThirdPartyTypesItemAdapter(ItemAdapter): +... ADAPTER_CLASSES = deque([AttrsAdapter, PydanticAdapter, ScrapyItemAdapter]) +... +>>> class ScrapyItem(Item): +... foo = Field() +... +>>> BuiltinTypesItemAdapter.is_item(dict()) +True +>>> ThirdPartyTypesItemAdapter.is_item(dict()) +False +>>> BuiltinTypesItemAdapter.is_item(ScrapyItem(foo="bar")) +False +>>> ThirdPartyTypesItemAdapter.is_item(ScrapyItem(foo="bar")) +True +>>> +``` + --- ## More examples diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/itemadapter/__init__.py new/itemadapter-0.8.0/itemadapter/__init__.py --- old/itemadapter-0.7.0/itemadapter/__init__.py 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/itemadapter/__init__.py 2023-03-30 21:09:01.000000000 +0200 @@ -2,4 +2,4 @@ from .utils import get_field_meta_from_class, is_item # noqa: F401 -__version__ = "0.7.0" +__version__ = "0.8.0" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/itemadapter/_imports.py new/itemadapter-0.8.0/itemadapter/_imports.py --- old/itemadapter-0.7.0/itemadapter/_imports.py 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/itemadapter/_imports.py 2023-03-30 21:09:01.000000000 +0200 @@ -18,11 +18,6 @@ _scrapy_item_classes = (scrapy.item.Item, _base_item_cls) try: - import dataclasses # pylint: disable=W0611 (unused-import) -except ImportError: - dataclasses = None # type: ignore [assignment] - -try: import attr # pylint: disable=W0611 (unused-import) except ImportError: attr = None # type: ignore [assignment] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/itemadapter/adapter.py new/itemadapter-0.8.0/itemadapter/adapter.py --- old/itemadapter-0.7.0/itemadapter/adapter.py 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/itemadapter/adapter.py 2023-03-30 21:09:01.000000000 +0200 @@ -1,3 +1,4 @@ +import dataclasses from abc import abstractmethod, ABCMeta from collections import deque from collections.abc import KeysView, MutableMapping @@ -7,11 +8,10 @@ from itemadapter.utils import ( _get_pydantic_model_metadata, _is_attrs_class, - _is_dataclass, _is_pydantic_model, ) -from itemadapter._imports import attr, dataclasses, _scrapy_item_classes +from itemadapter._imports import attr, _scrapy_item_classes __all__ = [ @@ -139,23 +139,19 @@ class DataclassAdapter(_MixinAttrsDataclassAdapter, AdapterInterface): def __init__(self, item: Any) -> None: super().__init__(item) - if dataclasses is None: - raise RuntimeError("dataclasses module is not available") # store a reference to the item's fields to avoid O(n) lookups and O(n^2) traversals self._fields_dict = {field.name: field for field in dataclasses.fields(self.item)} @classmethod def is_item(cls, item: Any) -> bool: - return _is_dataclass(item) and not isinstance(item, type) + return dataclasses.is_dataclass(item) and not isinstance(item, type) @classmethod def is_item_class(cls, item_class: type) -> bool: - return _is_dataclass(item_class) + return dataclasses.is_dataclass(item_class) @classmethod def get_field_meta_from_class(cls, item_class: type, field_name: str) -> MappingProxyType: - if dataclasses is None: - raise RuntimeError("dataclasses module is not available") for field in dataclasses.fields(item_class): if field.name == field_name: return field.metadata # type: ignore @@ -163,8 +159,6 @@ @classmethod def get_field_names_from_class(cls, item_class: type) -> Optional[List[str]]: - if dataclasses is None: - raise RuntimeError("dataclasses module is not available") return [a.name for a in dataclasses.fields(item_class)] @@ -361,17 +355,16 @@ """Return a dict object with the contents of the adapter. This works slightly different than calling `dict(adapter)`: it's applied recursively to nested items (if there are any). """ - return {key: _asdict(value) for key, value in self.items()} + return {key: self._asdict(value) for key, value in self.items()} - -def _asdict(obj: Any) -> Any: - """Helper for ItemAdapter.asdict().""" - if isinstance(obj, dict): - return {key: _asdict(value) for key, value in obj.items()} - if isinstance(obj, (list, set, tuple)): - return obj.__class__(_asdict(x) for x in obj) - if isinstance(obj, ItemAdapter): - return obj.asdict() - if ItemAdapter.is_item(obj): - return ItemAdapter(obj).asdict() - return obj + @classmethod + def _asdict(cls, obj: Any) -> Any: + if isinstance(obj, dict): + return {key: cls._asdict(value) for key, value in obj.items()} + if isinstance(obj, (list, set, tuple)): + return obj.__class__(cls._asdict(x) for x in obj) + if isinstance(obj, cls): + return obj.asdict() + if cls.is_item(obj): + return cls(obj).asdict() + return obj diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/itemadapter/utils.py new/itemadapter-0.8.0/itemadapter/utils.py --- old/itemadapter-0.7.0/itemadapter/utils.py 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/itemadapter/utils.py 2023-03-30 21:09:01.000000000 +0200 @@ -3,19 +3,12 @@ from types import MappingProxyType from typing import Any -from itemadapter._imports import attr, dataclasses, pydantic +from itemadapter._imports import attr, pydantic __all__ = ["is_item", "get_field_meta_from_class"] -def _is_dataclass(obj: Any) -> bool: - """In py36, this returns False if the "dataclasses" backport module is not installed.""" - if dataclasses is None: - return False - return dataclasses.is_dataclass(obj) - - def _is_attrs_class(obj: Any) -> bool: if attr is None: return False diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/pylintrc new/itemadapter-0.8.0/pylintrc --- old/itemadapter-0.7.0/pylintrc 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/pylintrc 2023-03-30 21:09:01.000000000 +0200 @@ -7,6 +7,7 @@ missing-function-docstring, missing-module-docstring, raise-missing-from, + too-many-return-statements, unused-argument, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/setup.py new/itemadapter-0.8.0/setup.py --- old/itemadapter-0.7.0/setup.py 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/setup.py 2023-03-30 21:09:01.000000000 +0200 @@ -7,7 +7,7 @@ setuptools.setup( name="itemadapter", - version="0.7.0", + version="0.8.0", license="BSD", description="Common interface for data container classes", long_description=long_description, @@ -16,16 +16,20 @@ author_email="[email protected]", url="https://github.com/scrapy/itemadapter", packages=["itemadapter"], - python_requires=">=3.6", + package_data={ + "itemadapter": ["py.typed"], + }, + include_package_data=True, + python_requires=">=3.7", classifiers=[ "Development Status :: 3 - Alpha", "License :: OSI Approved :: BSD License", "Programming Language :: Python", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Framework :: Scrapy", "Intended Audience :: Developers", "Topic :: Internet :: WWW/HTTP", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/tests/__init__.py new/itemadapter-0.8.0/tests/__init__.py --- old/itemadapter-0.7.0/tests/__init__.py 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/tests/__init__.py 2023-03-30 21:09:01.000000000 +0200 @@ -1,7 +1,8 @@ import importlib import sys from contextlib import contextmanager -from typing import Callable, Optional +from dataclasses import dataclass, field +from typing import Callable, Generator, Optional from itemadapter import ItemAdapter @@ -17,7 +18,7 @@ @contextmanager -def clear_itemadapter_imports() -> None: +def clear_itemadapter_imports() -> Generator[None, None, None]: backup = {} for key in sys.modules.copy().keys(): if key.startswith("itemadapter"): @@ -28,6 +29,39 @@ sys.modules.update(backup) +@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(init=False) +class DataClassWithoutInit: + name: str = field(metadata={"serializer": str}) + value: int = field(metadata={"serializer": int}) + + +@dataclass +class DataClassItemSubclassed(DataClassItem): + subclassed: bool = True + + +@dataclass +class DataClassItemEmpty: + pass + + try: import attr except ImportError: @@ -67,45 +101,6 @@ pass -try: - from dataclasses import dataclass, field -except ImportError: - DataClassItem = None - DataClassItemNested = None - DataClassWithoutInit = None - DataClassItemSubclassed = None - DataClassItemEmpty = 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(init=False) - class DataClassWithoutInit: - name: str = field(metadata={"serializer": str}) - value: int = field(metadata={"serializer": int}) - - @dataclass - class DataClassItemSubclassed(DataClassItem): - subclassed: bool = True - - @dataclass - class DataClassItemEmpty: - pass - - try: from pydantic import BaseModel, Field as PydanticField except ImportError: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/tests/requirements.txt new/itemadapter-0.8.0/tests/requirements.txt --- old/itemadapter-0.7.0/tests/requirements.txt 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/tests/requirements.txt 2023-03-30 21:09:01.000000000 +0200 @@ -1,5 +1,4 @@ attrs -dataclasses; python_version == "3.6" pydantic pytest-cov>=2.8 pytest>=5.4 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/tests/test_adapter_dataclasses.py new/itemadapter-0.8.0/tests/test_adapter_dataclasses.py --- old/itemadapter-0.7.0/tests/test_adapter_dataclasses.py 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/tests/test_adapter_dataclasses.py 2023-03-30 21:09:01.000000000 +0200 @@ -1,7 +1,6 @@ -import unittest import warnings from types import MappingProxyType -from unittest import mock +from unittest import TestCase from itemadapter.utils import get_field_meta_from_class @@ -11,12 +10,10 @@ PydanticModel, ScrapyItem, ScrapySubclassedItem, - make_mock_import, - clear_itemadapter_imports, ) -class DataclassTestCase(unittest.TestCase): +class DataclassTestCase(TestCase): def test_false(self): from itemadapter.adapter import DataclassAdapter @@ -36,33 +33,6 @@ 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__", make_mock_import("dataclasses")) - def test_module_import_error(self): - with clear_itemadapter_imports(): - from itemadapter.adapter import DataclassAdapter - - self.assertFalse(DataclassAdapter.is_item(DataClassItem(name="asdf", value=1234))) - with self.assertRaises(RuntimeError, msg="dataclasses module is not available"): - DataclassAdapter(DataClassItem(name="asdf", value=1234)) - with self.assertRaises(RuntimeError, msg="dataclasses module is not available"): - DataclassAdapter.get_field_meta_from_class(DataClassItem, "name") - with self.assertRaises(RuntimeError, msg="dataclasses module is not available"): - DataclassAdapter.get_field_names_from_class(DataClassItem) - - 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") - @mock.patch("itemadapter.utils.dataclasses", None) - def test_module_not_available(self): - from itemadapter.adapter import DataclassAdapter - - 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): from itemadapter.adapter import DataclassAdapter diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/tests/test_itemadapter.py new/itemadapter-0.8.0/tests/test_itemadapter.py --- old/itemadapter-0.7.0/tests/test_itemadapter.py 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/tests/test_itemadapter.py 2023-03-30 21:09:01.000000000 +0200 @@ -1,10 +1,11 @@ import unittest +from collections import deque -from itemadapter.adapter import ItemAdapter +from itemadapter.adapter import ItemAdapter, DictAdapter -class SubclassedItemAdapter(ItemAdapter): - pass +class DictOnlyItemAdapter(ItemAdapter): + ADAPTER_CLASSES = deque([DictAdapter]) class ItemAdapterTestCase(unittest.TestCase): @@ -13,5 +14,5 @@ self.assertEqual(repr(adapter), "<ItemAdapter for dict(foo='bar')>") def test_repr_subclass(self): - adapter = SubclassedItemAdapter(dict(foo="bar")) - self.assertEqual(repr(adapter), "<SubclassedItemAdapter for dict(foo='bar')>") + adapter = DictOnlyItemAdapter(dict(foo="bar")) + self.assertEqual(repr(adapter), "<DictOnlyItemAdapter for dict(foo='bar')>") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/itemadapter-0.7.0/tox.ini new/itemadapter-0.8.0/tox.ini --- old/itemadapter-0.7.0/tox.ini 2022-08-02 19:43:01.000000000 +0200 +++ new/itemadapter-0.8.0/tox.ini 2023-03-30 21:09:01.000000000 +0200 @@ -25,15 +25,18 @@ [testenv:typing] basepython = python3 deps = - mypy==0.941 + mypy==0.991 + attrs + pydantic + scrapy commands = mypy --install-types --non-interactive \ - --show-error-codes --ignore-missing-imports --follow-imports=skip {posargs:itemadapter} + --show-error-codes --ignore-missing-imports {posargs:itemadapter} [testenv:black] basepython = python3 deps = - black>=19.10b0 + black==22.12.0 commands = black --check {posargs:itemadapter tests}
