Skip to content

Commit

Permalink
rfctr: prepare for adding metadata.orig_elements field (#2647)
Browse files Browse the repository at this point in the history
**Summary**
Some typing modernization in `elements.py` which will get changes to add
the `orig_elements` metadata field.

Also some additions to `unit_util.py` to enable simplified mocking that
will be required in the next PR.
  • Loading branch information
scanny authored Mar 14, 2024
1 parent d9e5574 commit 94535e3
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 62 deletions.
6 changes: 1 addition & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ repos:
hooks:
- id: ruff
args:
[
"--fix",
"--select=C4,COM,E,F,I,PLR0402,PT,SIM,UP015,UP018,UP032,UP034",
"--ignore=PT011,PT012,SIM117,COM812",
]
["--fix"]

- repo: https://github.com/pycqa/flake8
rev: 4.0.1
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## 0.12.7-dev2
## 0.12.7-dev3

### Enhancements

Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,8 @@ check-flake8-print:

.PHONY: check-ruff
check-ruff:
ruff . --select C4,COM,E,F,I,PLR0402,PT,SIM,UP015,UP018,UP032,UP034 --ignore COM812,PT011,PT012,SIM117
# -- ruff options are determined by pyproject.toml --
ruff .

.PHONY: check-autoflake
check-autoflake:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ select = [
]
ignore = [
"COM812", # -- over aggressively insists on trailing commas where not desireable --
"PT005", # -- flags mock fixtures with names intentionally matching private method name --
"PT011", # -- pytest.raises({exc}) too broad, use match param or more specific exception --
"PT012", # -- pytest.raises() block should contain a single simple statement --
"SIM117", # -- merge `with` statements for context managers that have same scope --
Expand Down
164 changes: 163 additions & 1 deletion test_unstructured/unit_utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
"""Utilities that ease unit-testing."""

from __future__ import annotations

import datetime as dt
import difflib
import pathlib
from typing import List, Optional
from typing import Any, List, Optional
from unittest.mock import (
ANY,
MagicMock,
Mock,
PropertyMock,
call,
create_autospec,
mock_open,
patch,
)

from pytest import FixtureRequest, LogCaptureFixture # noqa: PT013

from unstructured.documents.elements import Element
from unstructured.staging.base import elements_from_json, elements_to_json

__all__ = (
"ANY",
"FixtureRequest",
"LogCaptureFixture",
"MagicMock",
"Mock",
"call",
"class_mock",
"function_mock",
"initializer_mock",
"instance_mock",
"method_mock",
"property_mock",
)


def assert_round_trips_through_JSON(elements: List[Element]) -> None:
"""Raises AssertionError if `elements -> JSON -> List[Element] -> JSON` are not equal.
Expand Down Expand Up @@ -54,3 +83,136 @@ def example_doc_path(file_name: str) -> str:
def parse_optional_datetime(datetime_str: Optional[str]) -> Optional[dt.datetime]:
"""Parse `datetime_str` to a datetime.datetime instance or None if `datetime_str` is None."""
return dt.datetime.fromisoformat(datetime_str) if datetime_str else None


# ------------------------------------------------------------------------------------------------
# MOCKING FIXTURES
# ------------------------------------------------------------------------------------------------
# These allow full-featured and type-safe mocks to be created simply by adding a unit-test
# fixture.
# ------------------------------------------------------------------------------------------------


def class_mock(
request: FixtureRequest, q_class_name: str, autospec: bool = True, **kwargs: Any
) -> Mock:
"""Return mock patching class with qualified name `q_class_name`.
The mock is autospec'ed based on the patched class unless the optional argument `autospec` is
set to False. Any other keyword arguments are passed through to Mock(). Patch is reversed after
calling test returns.
"""
_patch = patch(q_class_name, autospec=autospec, **kwargs)
request.addfinalizer(_patch.stop)
return _patch.start()


def cls_attr_mock(
request: FixtureRequest,
cls: type,
attr_name: str,
name: str | None = None,
**kwargs: Any,
):
"""Return a mock for attribute `attr_name` on `cls`.
Patch is reversed after pytest uses it.
"""
name = request.fixturename if name is None else name
_patch = patch.object(cls, attr_name, name=name, **kwargs)
request.addfinalizer(_patch.stop)
return _patch.start()


def function_mock(
request: FixtureRequest, q_function_name: str, autospec: bool = True, **kwargs: Any
):
"""Return mock patching function with qualified name `q_function_name`.
Patch is reversed after calling test returns.
"""
_patch = patch(q_function_name, autospec=autospec, **kwargs)
request.addfinalizer(_patch.stop)
return _patch.start()


def initializer_mock(request: FixtureRequest, cls: type, autospec: bool = True, **kwargs: Any):
"""Return mock for __init__() method on `cls`.
The patch is reversed after pytest uses it.
"""
_patch = patch.object(cls, "__init__", autospec=autospec, return_value=None, **kwargs)
request.addfinalizer(_patch.stop)
return _patch.start()


def instance_mock(
request: FixtureRequest,
cls: type,
name: str | None = None,
spec_set: bool = True,
**kwargs: Any,
):
"""Return a mock for an instance of `cls` that draws its spec from the class.
The mock does not allow new attributes to be set on the instance. If `name` is missing or
|None|, the name of the returned |Mock| instance is set to *request.fixturename*. Additional
keyword arguments are passed through to the Mock() call that creates the mock.
"""
name = name if name is not None else request.fixturename
return create_autospec(cls, _name=name, spec_set=spec_set, instance=True, **kwargs)


def loose_mock(request: FixtureRequest, name: str | None = None, **kwargs: Any):
"""Return a "loose" mock, meaning it has no spec to constrain calls on it.
Additional keyword arguments are passed through to Mock(). If called without a name, it is
assigned the name of the fixture.
"""
if name is None:
name = request.fixturename
return Mock(name=name, **kwargs)


def method_mock(
request: FixtureRequest,
cls: type,
method_name: str,
autospec: bool = True,
**kwargs: Any,
):
"""Return mock for method `method_name` on `cls`.
The patch is reversed after pytest uses it.
"""
_patch = patch.object(cls, method_name, autospec=autospec, **kwargs)
request.addfinalizer(_patch.stop)
return _patch.start()


def open_mock(request: FixtureRequest, module_name: str, **kwargs: Any):
"""Return a mock for the builtin `open()` method in `module_name`."""
target = "%s.open" % module_name
_patch = patch(target, mock_open(), create=True, **kwargs)
request.addfinalizer(_patch.stop)
return _patch.start()


def property_mock(request: FixtureRequest, cls: type, prop_name: str, **kwargs: Any) -> Mock:
"""A mock for property `prop_name` on class `cls`.
Patch is reversed at the end of the test run.
"""
_patch = patch.object(cls, prop_name, new_callable=PropertyMock, **kwargs)
request.addfinalizer(_patch.stop)
return _patch.start()


def var_mock(request: FixtureRequest, q_var_name: str, **kwargs: Any):
"""Return a mock patching the variable with qualified name `q_var_name`.
Patch is reversed after calling test returns.
"""
_patch = patch(q_var_name, **kwargs)
request.addfinalizer(_patch.stop)
return _patch.start()
2 changes: 1 addition & 1 deletion unstructured/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.12.7-dev2" # pragma: no cover
__version__ = "0.12.7-dev3" # pragma: no cover
Loading

0 comments on commit 94535e3

Please sign in to comment.