diff --git a/README.md b/README.md index 531eaaf..4b886dd 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Currently supported types are: * [`dict`](https://docs.python.org/3/library/stdtypes.html#dict) * [`dataclass`](https://docs.python.org/3/library/dataclasses.html)-based classes * [`attrs`](https://www.attrs.org)-based classes -* [`pydantic`](https://pydantic-docs.helpmanual.io/)-based classes (`pydantic>=2` not yet supported) +* [`pydantic`](https://pydantic-docs.helpmanual.io/)-based classes Additionally, interaction with arbitrary types is supported, by implementing a pre-defined interface (see [extending `itemadapter`](#extending-itemadapter)). @@ -28,7 +28,7 @@ a pre-defined interface (see [extending `itemadapter`](#extending-itemadapter)). * [`scrapy`](https://scrapy.org/): optional, needed to interact with `scrapy` 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 (`pydantic>=2` not yet supported) + `pydantic`-based items --- @@ -196,7 +196,7 @@ The returned value is taken from the following sources, depending on the item ty for `dataclass`-based items * [`attr.Attribute.metadata`](https://www.attrs.org/en/stable/examples.html#metadata) for `attrs`-based items - * [`pydantic.fields.FieldInfo`](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) + * [`pydantic.fields.FieldInfo`](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.FieldInfo) for `pydantic`-based items #### class method `get_field_names_from_class(item_class: type) -> Optional[list[str]]` diff --git a/itemadapter/adapter.py b/itemadapter/adapter.py index 3105748..24326a3 100644 --- a/itemadapter/adapter.py +++ b/itemadapter/adapter.py @@ -179,24 +179,24 @@ def get_field_meta_from_class(cls, item_class: type, field_name: str) -> Mapping @classmethod def get_field_names_from_class(cls, item_class: type) -> Optional[List[str]]: - return list(item_class.__fields__.keys()) # type: ignore[attr-defined] + return list(item_class.model_fields.keys()) # type: ignore[attr-defined] def field_names(self) -> KeysView: - return KeysView(self.item.__fields__) + return KeysView(self.item.model_fields) def __getitem__(self, field_name: str) -> Any: - if field_name in self.item.__fields__: + if field_name in self.item.model_fields: return getattr(self.item, field_name) raise KeyError(field_name) def __setitem__(self, field_name: str, value: Any) -> None: - if field_name in self.item.__fields__: + if field_name in self.item.model_fields: setattr(self.item, field_name, value) else: raise KeyError(f"{self.item.__class__.__name__} does not support field: {field_name}") def __delitem__(self, field_name: str) -> None: - if field_name in self.item.__fields__: + if field_name in self.item.model_fields: try: delattr(self.item, field_name) except AttributeError: @@ -205,7 +205,7 @@ def __delitem__(self, field_name: str) -> None: raise KeyError(f"{self.item.__class__.__name__} does not support field: {field_name}") def __iter__(self) -> Iterator: - return iter(attr for attr in self.item.__fields__ if hasattr(self.item, attr)) + return iter(attr for attr in self.item.model_fields if hasattr(self.item, attr)) def __len__(self) -> int: return len(list(iter(self))) diff --git a/itemadapter/utils.py b/itemadapter/utils.py index 36b5d52..dfa93fd 100644 --- a/itemadapter/utils.py +++ b/itemadapter/utils.py @@ -23,30 +23,21 @@ def _is_pydantic_model(obj: Any) -> bool: def _get_pydantic_model_metadata(item_model: Any, field_name: str) -> MappingProxyType: metadata = {} - field = item_model.__fields__[field_name].field_info + field = item_model.model_fields[field_name] for attribute in [ "alias", "title", "description", - "const", - "gt", - "ge", - "lt", - "le", - "multiple_of", - "min_items", - "max_items", - "min_length", - "max_length", - "regex", ]: value = getattr(field, attribute) if value is not None: metadata[attribute] = value - if not field.allow_mutation: - metadata["allow_mutation"] = field.allow_mutation - metadata.update(field.extra) + if field.frozen is not None: + metadata["frozen"] = field.frozen + + if field.json_schema_extra is not None: + metadata.update(field.json_schema_extra) return MappingProxyType(metadata) diff --git a/tests/__init__.py b/tests/__init__.py index ef8b13e..53582c6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -102,7 +102,7 @@ class AttrsItemEmpty: try: - from pydantic import BaseModel, Field as PydanticField + from pydantic import ConfigDict, BaseModel, Field as PydanticField except ImportError: PydanticModel = None PydanticSpecialCasesModel = None @@ -125,11 +125,9 @@ class PydanticSpecialCasesModel(BaseModel): special_cases: Optional[int] = PydanticField( default_factory=lambda: None, alias="special_cases", - allow_mutation=False, + frozen=False, ) - - class Config: - validate_assignment = True + model_config = ConfigDict(validate_assignment=True) class PydanticModelNested(BaseModel): nested: PydanticModel @@ -139,9 +137,7 @@ class PydanticModelNested(BaseModel): set_: set tuple_: tuple int_: int - - class Config: - arbitrary_types_allowed = True + model_config = ConfigDict(arbitrary_types_allowed=True) class PydanticModelSubclassed(PydanticModel): subclassed: bool = PydanticField( diff --git a/tests/requirements.txt b/tests/requirements.txt index 4e41abe..214b6dc 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,5 +1,5 @@ attrs -pydantic<2 +pydantic>2 pytest-cov>=2.8 pytest>=5.4 scrapy>=2.0 diff --git a/tests/test_adapter_pydantic.py b/tests/test_adapter_pydantic.py index e51dea2..f6100a3 100644 --- a/tests/test_adapter_pydantic.py +++ b/tests/test_adapter_pydantic.py @@ -73,7 +73,7 @@ def test_true(self): ) self.assertEqual( get_field_meta_from_class(PydanticSpecialCasesModel, "special_cases"), - MappingProxyType({"alias": "special_cases", "allow_mutation": False}), + MappingProxyType({"alias": "special_cases", "frozen": False}), ) with self.assertRaises(KeyError, msg="PydanticModel does not support field: non_existent"): get_field_meta_from_class(PydanticModel, "non_existent")