Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions itemadapter/_json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re
from collections.abc import Iterable, Iterator, Mapping, Sequence
from collections.abc import Set as AbstractSet
from copy import copy
from copy import deepcopy
from enum import Enum
from functools import lru_cache
from textwrap import dedent
Expand Down Expand Up @@ -495,7 +495,7 @@ def _setdefault_attribute_docstrings_on_json_schema(

def base_json_schema_from_item_class(item_class: type) -> dict[str, Any]:
json_schema_extra = getattr(item_class, "__json_schema_extra__", {})
schema = copy(json_schema_extra)
schema = deepcopy(json_schema_extra)
schema.setdefault("type", "object")
schema.setdefault("additionalProperties", False)
return schema
Expand All @@ -513,7 +513,7 @@ def _json_schema_from_item_class(
if not fields_meta:
return schema
schema["properties"] = {
field_name: copy(field_meta.get("json_schema_extra", {}))
field_name: deepcopy(field_meta.get("json_schema_extra", {}))
for field_name, field_meta in fields_meta.items()
}
required = [
Expand Down Expand Up @@ -550,7 +550,7 @@ def _json_schema_from_attrs(item_class: type, state: _JsonSchemaState) -> dict[s
attrs.resolve_types(item_class) # Ensure field.type annotations are resolved

schema["properties"] = {
field.name: copy(field.metadata.get("json_schema_extra", {})) for field in fields
field.name: deepcopy(field.metadata.get("json_schema_extra", {})) for field in fields
}
default_factory_fields: set[str] = set()
for field in fields:
Expand Down Expand Up @@ -626,7 +626,7 @@ def _json_schema_from_typed_dict(item_class: type, state: _JsonSchemaState) -> d
required_keys = item_class.__required_keys__ # type: ignore[attr-defined]
for field_name, field_type in type_hints.items():
field_metadata, field_required = _split_typed_dict_hint(extra_type_hints[field_name])
prop = copy(field_metadata.get("json_schema_extra", {}))
prop = deepcopy(field_metadata.get("json_schema_extra", {}))
update_prop_from_type(prop, field_type, state)
schema["properties"][field_name] = prop
if field_required is None:
Expand All @@ -646,7 +646,7 @@ def _json_schema_from_dataclass(item_class: type, state: _JsonSchemaState) -> di
default_factory_fields = set()
if fields:
schema["properties"] = {
field.name: copy(field.metadata.get("json_schema_extra", {})) for field in fields
field.name: deepcopy(field.metadata.get("json_schema_extra", {})) for field in fields
}
for field in fields:
prop = schema["properties"][field.name]
Expand All @@ -664,7 +664,7 @@ def _json_schema_from_pydantic(
adapter: type[AdapterInterface], item_class: type, state: _JsonSchemaState | None = None
) -> dict[str, Any]:
state = state or _JsonSchemaState(adapter=adapter, containers={item_class})
schema: dict[str, Any] = copy(
schema: dict[str, Any] = deepcopy(
item_class.model_config.get("json_schema_extra", {}) # type: ignore[attr-defined]
)
extra = item_class.model_config.get("extra") # type: ignore[attr-defined]
Expand All @@ -678,7 +678,7 @@ def _json_schema_from_pydantic(
if not fields:
return schema
schema["properties"] = {
name: copy(metadata.get("json_schema_extra", {})) for name, metadata in fields.items()
name: deepcopy(metadata.get("json_schema_extra", {})) for name, metadata in fields.items()
}
default_factory_fields: set[str] = set()
for name, metadata in fields.items():
Expand Down
80 changes: 80 additions & 0 deletions tests/test_json_schema_overrides.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from dataclasses import dataclass, field
from typing import Annotated, TypedDict

import pytest

from itemadapter import ItemAdapter
from itemadapter._imports import attrs, pydantic, scrapy
from tests import requires_attrs, requires_pydantic, requires_scrapy


@pytest.mark.parametrize("allow_additional", [False, True])
Expand Down Expand Up @@ -42,3 +45,80 @@ class Product:
"type": "array",
"items": {"type": "object", "additionalProperties": False},
}


def make_dataclass(value_type, extra):
@dataclass
class Item:
value: value_type = field(metadata={"json_schema_extra": extra})

return Item


def make_attrs(value_type, extra):
@attrs.define
class Item:
value: value_type = attrs.field(metadata={"json_schema_extra": extra})

return Item


def make_typed_dict(value_type, extra):
class Item(TypedDict):
value: Annotated[value_type, {"json_schema_extra": extra}]

return Item


def make_pydantic(value_type, extra):
class Item(pydantic.BaseModel):
value: value_type = pydantic.Field(json_schema_extra=extra)

return Item


def make_scrapy(value_type, extra):
class Item(scrapy.Item):
value: value_type = scrapy.Field(json_schema_extra=extra)

return Item


@pytest.mark.parametrize(
"make_item",
[
make_dataclass,
pytest.param(make_attrs, marks=requires_attrs),
make_typed_dict,
pytest.param(make_pydantic, marks=requires_pydantic),
pytest.param(make_scrapy, marks=requires_scrapy),
],
)
def test_shared_field_json_schema_extra(make_item):
extra = {"items": {"description": "Tag."}}

str_schema = ItemAdapter.get_json_schema(make_item(list[str], extra))
int_schema = ItemAdapter.get_json_schema(make_item(list[int], extra))
assert extra == {"items": {"description": "Tag."}}
assert str_schema["properties"]["value"] == {
"type": "array",
"items": {"description": "Tag.", "type": "string"},
}
assert int_schema["properties"]["value"] == {
"type": "array",
"items": {"description": "Tag.", "type": "integer"},
}


def test_class_json_schema_extra_unchanged():
extra = {"properties": {"name": {}}}

@dataclass
class Product:
__json_schema_extra__ = extra
name = "Product"
"""Product name."""

schema = ItemAdapter.get_json_schema(Product)
assert schema["properties"] == {"name": {"description": "Product name."}}
assert extra == {"properties": {"name": {}}}
Loading