Files
docling-core/test/test_metadata.py
c73904e68e style: replace black, isort, flake8 and autoflake with ruff (#456)
* Added ruff to dev dependencies

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Added ruff settings to pyproject.toml as in docling

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Cleanup uf pyproject.toml

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Copied settings for ruff pre-commit hooks from docling

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Excluded test/data/** from ruff formatting / linting

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* ruff format

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Added some ignore statements to pyproject.toml such that ruff check raises fewer issues

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* ruff check --fix

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Ignored some more rules

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Fixed the rest of the errors that would only concern 1 - 3 files

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Added another ignore related to df for DataFrame names

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Modified CONTRIBUTING.md such that black / isort are replaced by ruff

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Added UP045 to ignore list such that Optional[...] does not raise

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Moved .flake8 configs to pyproject.toml

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Moved autoflake to be used with ruff

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Moved all .flake8 settings to pyproject.toml to be compatible with ruff (i.e. no separate [tool.flake8] section

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Removed flake8 from .pre-commit hooks

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Applied ruff format (again); formatted some files as the line-length = 120 equals now what was set for the .flake8 settings

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Set max-complexity to 30 (as was originally) in the pyproject.toml as one linting check would fail

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Adding PD901 to ignore list such that pre-commit hooks run fully again

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* Replaced dtype | None syntax by Optional[dtype] in remaining places

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>

* chore: fix 'test' ref in pyproject

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>

* style: remove typing List, Set, Tuple, Dict

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>

* style: remove UP015 check from ignore list

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>

* style: remove UP034 check from ignore list

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>

* style: normalize dashes in comments and docstrings

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>

* style: remove PD901 check from ignore list

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>

* style: remove C403 check from ignore list

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>

* style: remove C403, C413, C416 check from ignore list

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>

* style: remove E203, F811 check from ignore list

Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>

---------

Signed-off-by: Florian Schwarb <florian.schwarb@gmail.com>
Signed-off-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>
Co-authored-by: Florian Schwarb <florian.schwarb@gmail.com>
Co-authored-by: Cesar Berrospi Ramis <ceb@zurich.ibm.com>
2026-01-13 17:03:10 +01:00

282 lines
10 KiB
Python

from pathlib import Path
from typing import Any, Optional
import pytest
from pydantic import BaseModel
from typing_extensions import override
from docling_core.transforms.serializer.base import SerializationResult
from docling_core.transforms.serializer.common import create_ser_result
from docling_core.transforms.serializer.markdown import (
MarkdownDocSerializer,
MarkdownMetaSerializer,
MarkdownParams,
)
from docling_core.types.doc import (
BaseMeta,
DocItem,
DocItemLabel,
DoclingDocument,
GroupLabel,
MetaFieldName,
MetaUtils,
NodeItem,
RefItem,
SummaryMetaField,
)
from .test_data_gen_flag import GEN_TEST_DATA
class CustomCoordinates(BaseModel):
longitude: float
latitude: float
def test_metadata_usage() -> None:
src = Path("test/data/doc/dummy_doc_with_meta.yaml")
doc = DoclingDocument.load_from_yaml(filename=src)
first_pic = doc.pictures[0]
assert first_pic.meta
assert first_pic.meta.classification
assert first_pic.meta.classification.predictions
assert first_pic.meta.classification.predictions[0].confidence == 0.78
example_item: NodeItem = RefItem(cref="#/texts/2").resolve(doc=doc)
assert example_item.meta is not None
# add a custom metadata object to the item
value = CustomCoordinates(longitude=47.3769, latitude=8.5417)
target_name = example_item.meta.set_custom_field(namespace="my_corp", name="coords", value=value)
assert target_name == "my_corp__coords"
# save the document
exp_file = src.parent / f"{src.stem}_modified.yaml"
if GEN_TEST_DATA:
doc.save_as_yaml(filename=exp_file)
else:
expected = DoclingDocument.load_from_yaml(filename=exp_file)
assert doc.model_dump(mode="json") == expected.model_dump(mode="json")
# load back the document and read the custom metadata object
loaded_doc = DoclingDocument.load_from_yaml(filename=exp_file)
loaded_item: NodeItem = RefItem(cref="#/texts/2").resolve(doc=loaded_doc)
assert loaded_item.meta is not None
loaded_dict = loaded_item.meta.get_custom_part()[target_name]
loaded_value = CustomCoordinates.model_validate(loaded_dict)
# ensure the value is the same
assert loaded_value == value
def test_metadata_relaxed_migration() -> None:
src = Path("test/data/doc/dummy_doc_with_meta_2.yaml")
doc = DoclingDocument.load_from_yaml(filename=src)
first_pic = doc.pictures[0]
assert first_pic.meta
assert first_pic.meta.classification
assert first_pic.meta.classification.predictions
# check migration was skipped since respetive meta already present:
assert first_pic.meta.classification.predictions[0].confidence == 0.42
def test_namespace_absence_raises():
src = Path("test/data/doc/dummy_doc_with_meta.yaml")
doc = DoclingDocument.load_from_yaml(filename=src)
example_item = RefItem(cref="#/texts/2").resolve(doc=doc)
with pytest.raises(ValueError):
example_item.meta.my_corp_programmaticaly_added_field = True
def _create_doc_with_group_with_metadata() -> DoclingDocument:
doc = DoclingDocument(name="")
doc.body.meta = BaseMeta(summary=SummaryMetaField(text="This document talks about various topics."))
grp1 = doc.add_group(name="1", label=GroupLabel.CHAPTER)
grp1.meta = BaseMeta(summary=SummaryMetaField(text="This chapter discusses foo and bar."))
doc.add_text(text="This is some introductory text.", label=DocItemLabel.TEXT, parent=grp1)
grp1a = doc.add_group(parent=grp1, name="1a", label=GroupLabel.SECTION)
grp1a.meta = BaseMeta(summary=SummaryMetaField(text="This section talks about foo."))
grp1a.meta.set_custom_field(namespace="my_corp", name="test_1", value="custom field value 1")
txt1 = doc.add_text(text="Regarding foo...", label=DocItemLabel.TEXT, parent=grp1a)
txt1.meta = BaseMeta(summary=SummaryMetaField(text="This paragraph provides more details about foo."))
lst1a = doc.add_list_group(parent=grp1a)
lst1a.meta = BaseMeta(summary=SummaryMetaField(text="Here some foo specifics are listed."))
doc.add_list_item(text="lorem", parent=lst1a, enumerated=True)
doc.add_list_item(text="ipsum", parent=lst1a, enumerated=True)
grp1b = doc.add_group(parent=grp1, name="1b", label=GroupLabel.SECTION)
grp1b.meta = BaseMeta(summary=SummaryMetaField(text="This section talks about bar."))
grp1b.meta.set_custom_field(namespace="my_corp", name="test_2", value="custom field value 2")
doc.add_text(text="Regarding bar...", label=DocItemLabel.TEXT, parent=grp1b)
return doc
def test_ser_deser():
doc = _create_doc_with_group_with_metadata()
# test dumping to and loading from YAML
exp_file = Path("test/data/doc/group_with_metadata.yaml")
if GEN_TEST_DATA:
doc.save_as_yaml(filename=exp_file)
else:
expected = DoclingDocument.load_from_yaml(filename=exp_file)
assert doc == expected
def test_md_ser_default():
doc = _create_doc_with_group_with_metadata()
# test exporting to Markdown
params = MarkdownParams()
ser = MarkdownDocSerializer(doc=doc, params=params)
ser_res = ser.serialize()
actual = ser_res.text
exp_file = Path("test/data/doc/group_with_metadata_default.md")
if GEN_TEST_DATA:
with open(exp_file, "w", encoding="utf-8") as f:
f.write(actual)
else:
with open(exp_file, "r", encoding="utf-8") as f:
expected = f.read()
assert actual == expected
def test_md_ser_marked():
doc = _create_doc_with_group_with_metadata()
# test exporting to Markdown
params = MarkdownParams(
mark_meta=True,
)
ser = MarkdownDocSerializer(doc=doc, params=params)
ser_res = ser.serialize()
actual = ser_res.text
exp_file = Path("test/data/doc/group_with_metadata_marked.md")
if GEN_TEST_DATA:
with open(exp_file, "w", encoding="utf-8") as f:
f.write(actual)
else:
with open(exp_file, "r", encoding="utf-8") as f:
expected = f.read()
assert actual == expected
def test_md_ser_allowed_meta_names():
doc = _create_doc_with_group_with_metadata()
params = MarkdownParams(
allowed_meta_names={
MetaUtils.create_meta_field_name(namespace="my_corp", name="test_1"),
},
mark_meta=True,
)
ser = MarkdownDocSerializer(doc=doc, params=params)
ser_res = ser.serialize()
actual = ser_res.text
exp_file = Path("test/data/doc/group_with_metadata_allowed_meta_names.md")
if GEN_TEST_DATA:
with open(exp_file, "w", encoding="utf-8") as f:
f.write(actual)
else:
with open(exp_file, "r", encoding="utf-8") as f:
expected = f.read()
assert actual == expected
def test_md_ser_blocked_meta_names():
doc = _create_doc_with_group_with_metadata()
params = MarkdownParams(
blocked_meta_names={
MetaUtils.create_meta_field_name(namespace="my_corp", name="test_1"),
MetaFieldName.SUMMARY.value,
},
mark_meta=True,
)
ser = MarkdownDocSerializer(doc=doc, params=params)
ser_res = ser.serialize()
actual = ser_res.text
exp_file = Path("test/data/doc/group_with_metadata_blocked_meta_names.md")
if GEN_TEST_DATA:
with open(exp_file, "w", encoding="utf-8") as f:
f.write(actual)
else:
with open(exp_file, "r", encoding="utf-8") as f:
expected = f.read()
assert actual == expected
def test_md_ser_without_non_meta():
doc = _create_doc_with_group_with_metadata()
params = MarkdownParams(
include_non_meta=False,
mark_meta=True,
)
ser = MarkdownDocSerializer(doc=doc, params=params)
ser_res = ser.serialize()
actual = ser_res.text
exp_file = Path("test/data/doc/group_with_metadata_without_non_meta.md")
if GEN_TEST_DATA:
with open(exp_file, "w", encoding="utf-8") as f:
f.write(actual)
else:
with open(exp_file, "r", encoding="utf-8") as f:
expected = f.read()
assert actual == expected
def test_ser_custom_meta_serializer():
class SummaryMarkdownMetaSerializer(MarkdownMetaSerializer):
@override
def serialize(
self,
*,
item: NodeItem,
doc: DoclingDocument,
level: Optional[int] = None,
**kwargs: Any,
) -> SerializationResult:
"""Serialize the item's meta."""
params = MarkdownParams(**kwargs)
return create_ser_result(
text="\n\n".join(
[
f"{' ' * (level or 0)}[{item.self_ref}] [{item.__class__.__name__}:{item.label.value}] {tmp}" # type:ignore[attr-defined]
for key in (list(item.meta.__class__.model_fields) + list(item.meta.get_custom_part()))
if (tmp := self._serialize_meta_field(item.meta, key, params.mark_meta))
]
if item.meta
else []
),
span_source=item if isinstance(item, DocItem) else [],
)
def _serialize_meta_field(self, meta: BaseMeta, name: str, mark_meta: bool) -> Optional[str]:
if (field_val := getattr(meta, name)) is not None and isinstance(field_val, SummaryMetaField):
txt = field_val.text
return f"[{self._humanize_text(name, title=True)}] {txt}" if mark_meta else txt
else:
return None
doc = _create_doc_with_group_with_metadata()
# test exporting to Markdown
params = MarkdownParams(
include_non_meta=False,
)
ser = MarkdownDocSerializer(doc=doc, params=params, meta_serializer=SummaryMarkdownMetaSerializer())
ser_res = ser.serialize()
actual = ser_res.text
exp_file = Path("test/data/doc/group_with_metadata_summaries.md")
if GEN_TEST_DATA:
with open(exp_file, "w", encoding="utf-8") as f:
f.write(actual)
else:
with open(exp_file, "r", encoding="utf-8") as f:
expected = f.read()
assert actual == expected