refactor: simplifies JSON detection and add tests (#975)

* refactor json detection

* version and changelog

* fix mock in test
This commit is contained in:
Matt Robinson 2023-07-25 15:59:45 -04:00 committed by GitHub
parent f282a10715
commit d694cd53bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 65 additions and 19 deletions

View File

@ -1,11 +1,12 @@
## 0.8.2-dev6
## 0.8.2-dev7
### Enhancements
* Additional tests and refactor of JSON detection.
* Update functionality to retrieve image metadata from a page for `document_to_element_list`
* Links are now tracked in `partition_html` output.
* Set the file's current position to the beginning after reading the file in `convert_to_bytes`
* Add min_partition kwarg to that combines elements below a specified threshold and modifies splitting of strings longer than max partition so words are not split.
* Add `min_partition` kwarg to that combines elements below a specified threshold and modifies splitting of strings longer than max partition so words are not split.
* set the file's current position to the beginning after reading the file in `convert_to_bytes`
* Add slide notes to pptx
* Add `--encoding` directive to ingest

View File

@ -77,7 +77,7 @@ class MockDocumentLayout(layout.DocumentLayout):
("README.rst", FileType.RST),
("README.md", FileType.MD),
("fake.odt", FileType.ODT),
("fake-incomplete-json.txt", FileType.JSON),
("fake-incomplete-json.txt", FileType.TXT),
],
)
def test_detect_filetype_from_filename(file, expected):
@ -141,7 +141,7 @@ def test_detect_filetype_from_filename_with_extension(monkeypatch, file, expecte
("stanley-cups.tsv", FileType.TSV),
("fake-power-point.pptx", FileType.PPTX),
("winter-sports.epub", FileType.EPUB),
("fake-incomplete-json.txt", FileType.JSON),
("fake-incomplete-json.txt", FileType.TXT),
],
)
def test_detect_filetype_from_file(file, expected):

View File

@ -220,6 +220,19 @@ def test_auto_partition_json_from_filename():
assert json_data == json_elems
def test_auto_partition_json_raises_with_unprocessable_json(tmpdir):
# NOTE(robinson) - This is unprocessable because it is not a list of dicts,
# per the Unstructured ISD format
text = '{"hi": "there"}'
filename = os.path.join(tmpdir, "unprocessable.json")
with open(filename, "w") as f:
f.write(text)
with pytest.raises(ValueError):
partition(filename=filename)
@pytest.mark.xfail(
reason="parsed as text not json, https://github.com/Unstructured-IO/unstructured/issues/492",
)
@ -525,7 +538,7 @@ def test_auto_partition_odt_from_file():
@pytest.mark.parametrize(
("content_type", "routing_func", "expected"),
[
("application/json", "json", "application/json"),
("text/csv", "csv", "text/csv"),
("text/html", "html", "text/html"),
("jdsfjdfsjkds", "pdf", None),
],

View File

@ -204,3 +204,17 @@ def test_partition_json_from_text_exclude_metadata(filename: str):
for i in range(len(test_elements)):
assert any(test_elements[i].metadata.to_dict()) is False
def test_partition_json_raises_with_unprocessable_json():
# NOTE(robinson) - This is unprocessable because it is not a list of dicts,
# per the Unstructured ISD format
text = '{"hi": "there"}'
with pytest.raises(ValueError):
partition_json(text=text)
def test_partition_json_raises_with_invalid_json():
text = '[{"hi": "there"}]]'
with pytest.raises(ValueError):
partition_json(text=text)

View File

@ -1 +1 @@
__version__ = "0.8.2-dev6" # pragma: no cover
__version__ = "0.8.2-dev7" # pragma: no cover

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import inspect
import json
import os
import re
import zipfile
@ -11,7 +12,7 @@ from typing import IO, TYPE_CHECKING, Callable, List, Optional
from unstructured.documents.coordinates import PixelSpace
from unstructured.documents.elements import Element, PageBreak
from unstructured.file_utils.encoding import detect_file_encoding, format_encoding_str
from unstructured.nlp.patterns import JSON_PATTERN, VALID_JSON_CHARACTERS
from unstructured.nlp.patterns import LIST_OF_DICTS_PATTERN
from unstructured.partition.common import (
_add_element_metadata,
_remove_element_metadata,
@ -417,15 +418,23 @@ def _is_text_file_a_json(
):
"""Detects if a file that has a text/plain MIME type is a JSON file."""
file_text = _read_file_start_for_type_check(file=file, filename=filename, encoding=encoding)
text_without_strings = re.sub(r'"(?:\\.|[^"\\])*"', "", file_text)
if not re.match(VALID_JSON_CHARACTERS, text_without_strings):
try:
json.loads(file_text)
return True
except json.JSONDecodeError:
return False
if not re.match(JSON_PATTERN, file_text):
return False
return True
def is_json_processable(
filename: Optional[str] = None,
file: Optional[IO[bytes]] = None,
file_text: Optional[str] = None,
encoding: Optional[str] = "utf-8",
) -> bool:
exactly_one(filename=filename, file=file, file_text=file_text)
if file_text is None:
file_text = _read_file_start_for_type_check(file=file, filename=filename, encoding=encoding)
return re.match(LIST_OF_DICTS_PATTERN, file_text) is not None
def _count_commas(text: str):

View File

@ -9,6 +9,7 @@ from unstructured.file_utils.filetype import (
STR_TO_FILETYPE,
FileType,
detect_filetype,
is_json_processable,
)
from unstructured.logger import logger
from unstructured.partition.common import exactly_one
@ -227,6 +228,11 @@ def partition(
**kwargs,
)
elif filetype == FileType.JSON:
if not is_json_processable(filename=filename, file=file):
raise ValueError(
"Detected a JSON file that does not conform to the Unstructured schema. "
"partition_json currently only processes serialized Unstructured output.",
)
elements = partition_json(filename=filename, file=file, **kwargs)
elif (filetype == FileType.XLSX) or (filetype == FileType.XLS):
elements = partition_xlsx(filename=filename, file=file, **kwargs)

View File

@ -1,10 +1,12 @@
import json
import re
from typing import IO, List, Optional
from unstructured.documents.elements import Element, process_metadata
from unstructured.file_utils.filetype import FileType, add_metadata_with_filetype
from unstructured.nlp.patterns import LIST_OF_DICTS_PATTERN
from unstructured.file_utils.filetype import (
FileType,
add_metadata_with_filetype,
is_json_processable,
)
from unstructured.partition.common import exactly_one
from unstructured.staging.base import dict_to_elements
@ -48,9 +50,10 @@ def partition_json(
elif text is not None:
file_text = str(text)
# NOTE(Nathan): we expect file_text to be a list of dicts (optimization)
if not re.match(LIST_OF_DICTS_PATTERN, file_text):
raise ValueError("Json schema does not match the Unstructured schema")
if not is_json_processable(file_text=file_text):
raise ValueError(
"JSON cannot be partitioned. Schema does not match the Unstructured schema.",
)
try:
dict = json.loads(file_text)