mirror of https://github.com/scrapy/scrapy.git
156 lines
5.3 KiB
Python
156 lines
5.3 KiB
Python
"""
|
|
Scrapy Item
|
|
|
|
See documentation in docs/topics/items.rst
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABCMeta
|
|
from collections.abc import MutableMapping
|
|
from copy import deepcopy
|
|
from pprint import pformat
|
|
from typing import TYPE_CHECKING, Any, NoReturn
|
|
|
|
from scrapy.utils.trackref import object_ref
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator, KeysView
|
|
|
|
# typing.Self requires Python 3.11
|
|
from typing_extensions import Self
|
|
|
|
|
|
class Field(dict[str, Any]):
|
|
"""Container of field metadata"""
|
|
|
|
|
|
def _ordered_field_names(cls: type) -> list[str]:
|
|
"""Return the names of the :class:`Field` attributes of *cls* in definition
|
|
order.
|
|
|
|
Fields declared in base classes come first, ordered from the topmost base
|
|
to the most derived class. Within each class, fields keep their definition
|
|
order. A field redefined in a subclass keeps the position of its first
|
|
definition.
|
|
"""
|
|
names: list[str] = []
|
|
seen: set[str] = set()
|
|
for base in reversed(cls.__mro__):
|
|
for name, value in vars(base).items():
|
|
if isinstance(value, Field) and name not in seen:
|
|
seen.add(name)
|
|
names.append(name)
|
|
return names
|
|
|
|
|
|
class ItemMeta(ABCMeta):
|
|
"""Metaclass_ of :class:`Item` that handles field definitions.
|
|
|
|
.. _metaclass: https://realpython.com/python-metaclasses
|
|
"""
|
|
|
|
def __new__(
|
|
mcs, class_name: str, bases: tuple[type, ...], attrs: dict[str, Any]
|
|
) -> ItemMeta:
|
|
classcell = attrs.pop("__classcell__", None)
|
|
new_bases = tuple(base._class for base in bases if hasattr(base, "_class"))
|
|
_class = super().__new__(mcs, "x_" + class_name, new_bases, attrs)
|
|
|
|
fields = getattr(_class, "fields", {})
|
|
for n in _ordered_field_names(_class):
|
|
fields[n] = getattr(_class, n)
|
|
new_attrs = {n: v for n, v in attrs.items() if not isinstance(v, Field)}
|
|
|
|
new_attrs["fields"] = fields
|
|
new_attrs["_class"] = _class
|
|
if classcell is not None:
|
|
new_attrs["__classcell__"] = classcell
|
|
return super().__new__(mcs, class_name, bases, new_attrs)
|
|
|
|
|
|
class Item(MutableMapping[str, Any], object_ref, metaclass=ItemMeta):
|
|
"""Base class for scraped items.
|
|
|
|
In Scrapy, an object is considered an ``item`` if it's supported by the
|
|
`itemadapter`_ library. For example, when the output of a spider callback
|
|
is evaluated, only such objects are passed to :ref:`item pipelines
|
|
<topics-item-pipeline>`. :class:`Item` is one of the classes supported by
|
|
`itemadapter`_ by default.
|
|
|
|
Items must declare :class:`Field` attributes, which are processed and stored
|
|
in the ``fields`` attribute. This restricts the set of allowed field names
|
|
and prevents typos, raising ``KeyError`` when referring to undefined fields.
|
|
Additionally, fields can be used to define metadata and control the way
|
|
data is processed internally. Please refer to the :ref:`documentation
|
|
about fields <topics-items-fields>` for additional information.
|
|
|
|
Unlike instances of :class:`dict`, instances of :class:`Item` may be
|
|
:ref:`tracked <topics-leaks-trackrefs>` to debug memory leaks.
|
|
|
|
.. _itemadapter: https://github.com/scrapy/itemadapter
|
|
"""
|
|
|
|
#: A dictionary containing *all declared fields* for this Item, not only
|
|
#: those populated. The keys are the field names and the values are the
|
|
#: :class:`Field` objects used in the :ref:`Item declaration
|
|
#: <topics-items-declaring>`.
|
|
#:
|
|
#: Fields are kept in definition order: fields declared in base classes
|
|
#: come first, followed by fields declared in subclasses, and a field
|
|
#: redefined in a subclass keeps the position of its first definition.
|
|
#:
|
|
#: .. versionchanged:: 2.17.0
|
|
#: Fields are now returned in definition order rather than alphabetical
|
|
#: order.
|
|
fields: dict[str, Field]
|
|
|
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
self._values: dict[str, Any] = {}
|
|
if args or kwargs: # avoid creating dict for most common case
|
|
for k, v in dict(*args, **kwargs).items():
|
|
self[k] = v
|
|
|
|
def __getitem__(self, key: str) -> Any:
|
|
return self._values[key]
|
|
|
|
def __setitem__(self, key: str, value: Any) -> None:
|
|
if key in self.fields:
|
|
self._values[key] = value
|
|
else:
|
|
raise KeyError(f"{self.__class__.__name__} does not support field: {key}")
|
|
|
|
def __delitem__(self, key: str) -> None:
|
|
del self._values[key]
|
|
|
|
def __getattr__(self, name: str) -> NoReturn:
|
|
if name in self.fields:
|
|
raise AttributeError(f"Use item[{name!r}] to get field value")
|
|
raise AttributeError(name)
|
|
|
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
if not name.startswith("_"):
|
|
raise AttributeError(f"Use item[{name!r}] = {value!r} to set field value")
|
|
super().__setattr__(name, value)
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._values)
|
|
|
|
def __iter__(self) -> Iterator[str]:
|
|
return iter(self._values)
|
|
|
|
__hash__ = object_ref.__hash__
|
|
|
|
def keys(self) -> KeysView[str]:
|
|
return self._values.keys()
|
|
|
|
def __repr__(self) -> str:
|
|
return pformat(dict(self))
|
|
|
|
def copy(self) -> Self:
|
|
return self.__class__(self)
|
|
|
|
def deepcopy(self) -> Self:
|
|
"""Return a :func:`~copy.deepcopy` of this item."""
|
|
return deepcopy(self)
|