2023-10-12 12:47:55 -07:00
|
|
|
"""Utilities that ease unit-testing."""
|
|
|
|
|
2024-03-14 14:31:58 -07:00
|
|
|
from __future__ import annotations
|
|
|
|
|
Dynamic ElementMetadata implementation (#2043)
### Executive Summary
The structure of element metadata is currently static, meaning only
predefined fields can appear in the metadata. We would like the
flexibility for end-users, at their own discretion, to define and use
additional metadata fields that make sense for their particular
use-case.
### Concepts
A key concept for dynamic metadata is _known field_. A known-field is
one of those explicitly defined on `ElementMetadata`. Each of these has
a type and can be specified when _constructing_ a new `ElementMetadata`
instance. This is in contrast to an _end-user defined_ (or _ad-hoc_)
metadata field, one not known at "compile" time and added at the
discretion of an end-user to suit the purposes of their application.
An ad-hoc field can only be added by _assignment_ on an already
constructed instance.
### End-user ad-hoc metadata field behaviors
An ad-hoc field can be added to an `ElementMetadata` instance by
assignment:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
```
A field added in this way can be accessed by name:
```python
>>> metadata.coefficient
0.536
```
and that field will appear in the JSON/dict for that instance:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
>>> metadata.to_dict()
{"coefficient": 0.536}
```
However, accessing a "user-defined" value that has _not_ been assigned
on that instance raises `AttributeError`:
```python
>>> metadata.coeffcient # -- misspelled "coefficient" --
AttributeError: 'ElementMetadata' object has no attribute 'coeffcient'
```
This makes "tagging" a metadata item with a value very convenient, but
entails the proviso that if an end-user wants to add a metadata field to
_some_ elements and not others (sparse population), AND they want to
access that field by name on ANY element and receive `None` where it has
not been assigned, they will need to use an expression like this:
```python
coefficient = metadata.coefficient if hasattr(metadata, "coefficient") else None
```
### Implementation Notes
- **ad-hoc metadata fields** are discarded during consolidation (for
chunking) because we don't have a consolidation strategy defined for
those. We could consider using a default consolidation strategy like
`FIRST` or possibly allow a user to register a strategy (although that
gets hairy in non-private and multiple-memory-space situations.)
- ad-hoc metadata fields **cannot start with an underscore**.
- We have no way to distinguish an ad-hoc field from any "noise" fields
that might appear in a JSON/dict loaded using `.from_dict()`, so unlike
the original (which only loaded known-fields), we'll rehydrate anything
that we find there.
- No real type-safety is possible on ad-hoc fields but the type-checker
does not complain because the type of all ad-hoc fields is `Any` (which
is the best available behavior in my view).
- We may want to consider whether end-users should be able to add ad-hoc
fields to "sub" metadata objects too, like `DataSourceMetadata` and
conceivably `CoordinatesMetadata` (although I'm not immediately seeing a
use-case for the second one).
2023-11-15 13:22:15 -08:00
|
|
|
import datetime as dt
|
|
|
|
import difflib
|
2023-10-12 12:47:55 -07:00
|
|
|
import pathlib
|
2024-08-27 18:02:24 -04:00
|
|
|
import types
|
2024-03-14 14:31:58 -07:00
|
|
|
from typing import Any, List, Optional
|
|
|
|
from unittest.mock import (
|
|
|
|
ANY,
|
|
|
|
MagicMock,
|
|
|
|
Mock,
|
|
|
|
PropertyMock,
|
|
|
|
call,
|
|
|
|
create_autospec,
|
|
|
|
mock_open,
|
|
|
|
patch,
|
|
|
|
)
|
|
|
|
|
2024-05-29 14:36:05 -07:00
|
|
|
from pytest import CaptureFixture, FixtureRequest, LogCaptureFixture, MonkeyPatch # noqa: PT013
|
2023-10-12 12:47:55 -07:00
|
|
|
|
|
|
|
from unstructured.documents.elements import Element
|
|
|
|
from unstructured.staging.base import elements_from_json, elements_to_json
|
|
|
|
|
2024-03-14 14:31:58 -07:00
|
|
|
__all__ = (
|
|
|
|
"ANY",
|
2024-05-15 11:32:51 -07:00
|
|
|
"CaptureFixture",
|
2024-03-14 14:31:58 -07:00
|
|
|
"FixtureRequest",
|
|
|
|
"LogCaptureFixture",
|
|
|
|
"MagicMock",
|
|
|
|
"Mock",
|
2024-05-29 14:36:05 -07:00
|
|
|
"MonkeyPatch",
|
2024-03-14 14:31:58 -07:00
|
|
|
"call",
|
|
|
|
"class_mock",
|
|
|
|
"function_mock",
|
|
|
|
"initializer_mock",
|
|
|
|
"instance_mock",
|
|
|
|
"method_mock",
|
|
|
|
"property_mock",
|
|
|
|
)
|
|
|
|
|
2023-10-12 12:47:55 -07:00
|
|
|
|
|
|
|
def assert_round_trips_through_JSON(elements: List[Element]) -> None:
|
|
|
|
"""Raises AssertionError if `elements -> JSON -> List[Element] -> JSON` are not equal.
|
|
|
|
|
|
|
|
The procedure is:
|
|
|
|
|
|
|
|
1. Serialize `elements` to (original) JSON.
|
|
|
|
2. Deserialize that JSON to `List[Element]`.
|
|
|
|
3. Serialize that `List[Element]` to JSON.
|
|
|
|
3. Compare the original and round-tripped JSON, raise if they are different.
|
|
|
|
|
|
|
|
"""
|
|
|
|
original_json = elements_to_json(elements)
|
|
|
|
assert original_json is not None
|
|
|
|
|
|
|
|
round_tripped_elements = elements_from_json(text=original_json)
|
|
|
|
|
|
|
|
round_tripped_json = elements_to_json(round_tripped_elements)
|
|
|
|
assert round_tripped_json is not None
|
|
|
|
|
Dynamic ElementMetadata implementation (#2043)
### Executive Summary
The structure of element metadata is currently static, meaning only
predefined fields can appear in the metadata. We would like the
flexibility for end-users, at their own discretion, to define and use
additional metadata fields that make sense for their particular
use-case.
### Concepts
A key concept for dynamic metadata is _known field_. A known-field is
one of those explicitly defined on `ElementMetadata`. Each of these has
a type and can be specified when _constructing_ a new `ElementMetadata`
instance. This is in contrast to an _end-user defined_ (or _ad-hoc_)
metadata field, one not known at "compile" time and added at the
discretion of an end-user to suit the purposes of their application.
An ad-hoc field can only be added by _assignment_ on an already
constructed instance.
### End-user ad-hoc metadata field behaviors
An ad-hoc field can be added to an `ElementMetadata` instance by
assignment:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
```
A field added in this way can be accessed by name:
```python
>>> metadata.coefficient
0.536
```
and that field will appear in the JSON/dict for that instance:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
>>> metadata.to_dict()
{"coefficient": 0.536}
```
However, accessing a "user-defined" value that has _not_ been assigned
on that instance raises `AttributeError`:
```python
>>> metadata.coeffcient # -- misspelled "coefficient" --
AttributeError: 'ElementMetadata' object has no attribute 'coeffcient'
```
This makes "tagging" a metadata item with a value very convenient, but
entails the proviso that if an end-user wants to add a metadata field to
_some_ elements and not others (sparse population), AND they want to
access that field by name on ANY element and receive `None` where it has
not been assigned, they will need to use an expression like this:
```python
coefficient = metadata.coefficient if hasattr(metadata, "coefficient") else None
```
### Implementation Notes
- **ad-hoc metadata fields** are discarded during consolidation (for
chunking) because we don't have a consolidation strategy defined for
those. We could consider using a default consolidation strategy like
`FIRST` or possibly allow a user to register a strategy (although that
gets hairy in non-private and multiple-memory-space situations.)
- ad-hoc metadata fields **cannot start with an underscore**.
- We have no way to distinguish an ad-hoc field from any "noise" fields
that might appear in a JSON/dict loaded using `.from_dict()`, so unlike
the original (which only loaded known-fields), we'll rehydrate anything
that we find there.
- No real type-safety is possible on ad-hoc fields but the type-checker
does not complain because the type of all ad-hoc fields is `Any` (which
is the best available behavior in my view).
- We may want to consider whether end-users should be able to add ad-hoc
fields to "sub" metadata objects too, like `DataSourceMetadata` and
conceivably `CoordinatesMetadata` (although I'm not immediately seeing a
use-case for the second one).
2023-11-15 13:22:15 -08:00
|
|
|
assert round_tripped_json == original_json, _diff(
|
|
|
|
"JSON differs:", round_tripped_json, original_json
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-04-16 23:14:53 +02:00
|
|
|
def assign_hash_ids(elements: list[Element]) -> list[Element]:
|
|
|
|
"""Updates the `id` attribute of each element to a hash."""
|
2024-04-24 09:05:20 +02:00
|
|
|
for idx, element in enumerate(elements):
|
|
|
|
element.id_to_hash(idx)
|
2024-04-16 23:14:53 +02:00
|
|
|
return elements
|
|
|
|
|
|
|
|
|
Dynamic ElementMetadata implementation (#2043)
### Executive Summary
The structure of element metadata is currently static, meaning only
predefined fields can appear in the metadata. We would like the
flexibility for end-users, at their own discretion, to define and use
additional metadata fields that make sense for their particular
use-case.
### Concepts
A key concept for dynamic metadata is _known field_. A known-field is
one of those explicitly defined on `ElementMetadata`. Each of these has
a type and can be specified when _constructing_ a new `ElementMetadata`
instance. This is in contrast to an _end-user defined_ (or _ad-hoc_)
metadata field, one not known at "compile" time and added at the
discretion of an end-user to suit the purposes of their application.
An ad-hoc field can only be added by _assignment_ on an already
constructed instance.
### End-user ad-hoc metadata field behaviors
An ad-hoc field can be added to an `ElementMetadata` instance by
assignment:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
```
A field added in this way can be accessed by name:
```python
>>> metadata.coefficient
0.536
```
and that field will appear in the JSON/dict for that instance:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
>>> metadata.to_dict()
{"coefficient": 0.536}
```
However, accessing a "user-defined" value that has _not_ been assigned
on that instance raises `AttributeError`:
```python
>>> metadata.coeffcient # -- misspelled "coefficient" --
AttributeError: 'ElementMetadata' object has no attribute 'coeffcient'
```
This makes "tagging" a metadata item with a value very convenient, but
entails the proviso that if an end-user wants to add a metadata field to
_some_ elements and not others (sparse population), AND they want to
access that field by name on ANY element and receive `None` where it has
not been assigned, they will need to use an expression like this:
```python
coefficient = metadata.coefficient if hasattr(metadata, "coefficient") else None
```
### Implementation Notes
- **ad-hoc metadata fields** are discarded during consolidation (for
chunking) because we don't have a consolidation strategy defined for
those. We could consider using a default consolidation strategy like
`FIRST` or possibly allow a user to register a strategy (although that
gets hairy in non-private and multiple-memory-space situations.)
- ad-hoc metadata fields **cannot start with an underscore**.
- We have no way to distinguish an ad-hoc field from any "noise" fields
that might appear in a JSON/dict loaded using `.from_dict()`, so unlike
the original (which only loaded known-fields), we'll rehydrate anything
that we find there.
- No real type-safety is possible on ad-hoc fields but the type-checker
does not complain because the type of all ad-hoc fields is `Any` (which
is the best available behavior in my view).
- We may want to consider whether end-users should be able to add ad-hoc
fields to "sub" metadata objects too, like `DataSourceMetadata` and
conceivably `CoordinatesMetadata` (although I'm not immediately seeing a
use-case for the second one).
2023-11-15 13:22:15 -08:00
|
|
|
def _diff(heading: str, actual: str, expected: str):
|
|
|
|
"""Diff of actual compared to expected.
|
|
|
|
|
|
|
|
"+" indicates unexpected lines actual, "-" indicates lines missing from actual.
|
|
|
|
"""
|
|
|
|
expected_lines = expected.splitlines(keepends=True)
|
|
|
|
actual_lines = actual.splitlines(keepends=True)
|
|
|
|
heading = "diff: '+': unexpected lines in actual, '-': lines missing from actual\n"
|
|
|
|
return heading + "".join(difflib.Differ().compare(actual_lines, expected_lines))
|
2023-10-12 12:47:55 -07:00
|
|
|
|
|
|
|
|
|
|
|
def example_doc_path(file_name: str) -> str:
|
|
|
|
"""Resolve the absolute-path to `file_name` in the example-docs directory."""
|
|
|
|
example_docs_dir = pathlib.Path(__file__).parent.parent / "example-docs"
|
|
|
|
file_path = example_docs_dir / file_name
|
|
|
|
return str(file_path.resolve())
|
Dynamic ElementMetadata implementation (#2043)
### Executive Summary
The structure of element metadata is currently static, meaning only
predefined fields can appear in the metadata. We would like the
flexibility for end-users, at their own discretion, to define and use
additional metadata fields that make sense for their particular
use-case.
### Concepts
A key concept for dynamic metadata is _known field_. A known-field is
one of those explicitly defined on `ElementMetadata`. Each of these has
a type and can be specified when _constructing_ a new `ElementMetadata`
instance. This is in contrast to an _end-user defined_ (or _ad-hoc_)
metadata field, one not known at "compile" time and added at the
discretion of an end-user to suit the purposes of their application.
An ad-hoc field can only be added by _assignment_ on an already
constructed instance.
### End-user ad-hoc metadata field behaviors
An ad-hoc field can be added to an `ElementMetadata` instance by
assignment:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
```
A field added in this way can be accessed by name:
```python
>>> metadata.coefficient
0.536
```
and that field will appear in the JSON/dict for that instance:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
>>> metadata.to_dict()
{"coefficient": 0.536}
```
However, accessing a "user-defined" value that has _not_ been assigned
on that instance raises `AttributeError`:
```python
>>> metadata.coeffcient # -- misspelled "coefficient" --
AttributeError: 'ElementMetadata' object has no attribute 'coeffcient'
```
This makes "tagging" a metadata item with a value very convenient, but
entails the proviso that if an end-user wants to add a metadata field to
_some_ elements and not others (sparse population), AND they want to
access that field by name on ANY element and receive `None` where it has
not been assigned, they will need to use an expression like this:
```python
coefficient = metadata.coefficient if hasattr(metadata, "coefficient") else None
```
### Implementation Notes
- **ad-hoc metadata fields** are discarded during consolidation (for
chunking) because we don't have a consolidation strategy defined for
those. We could consider using a default consolidation strategy like
`FIRST` or possibly allow a user to register a strategy (although that
gets hairy in non-private and multiple-memory-space situations.)
- ad-hoc metadata fields **cannot start with an underscore**.
- We have no way to distinguish an ad-hoc field from any "noise" fields
that might appear in a JSON/dict loaded using `.from_dict()`, so unlike
the original (which only loaded known-fields), we'll rehydrate anything
that we find there.
- No real type-safety is possible on ad-hoc fields but the type-checker
does not complain because the type of all ad-hoc fields is `Any` (which
is the best available behavior in my view).
- We may want to consider whether end-users should be able to add ad-hoc
fields to "sub" metadata objects too, like `DataSourceMetadata` and
conceivably `CoordinatesMetadata` (although I'm not immediately seeing a
use-case for the second one).
2023-11-15 13:22:15 -08:00
|
|
|
|
|
|
|
|
2024-06-05 16:11:58 -07:00
|
|
|
def example_doc_text(file_name: str) -> str:
|
|
|
|
"""Contents of example-doc `file_name` as text (decoded as utf-8)."""
|
|
|
|
with open(example_doc_path(file_name)) as f:
|
|
|
|
return f.read()
|
|
|
|
|
|
|
|
|
Dynamic ElementMetadata implementation (#2043)
### Executive Summary
The structure of element metadata is currently static, meaning only
predefined fields can appear in the metadata. We would like the
flexibility for end-users, at their own discretion, to define and use
additional metadata fields that make sense for their particular
use-case.
### Concepts
A key concept for dynamic metadata is _known field_. A known-field is
one of those explicitly defined on `ElementMetadata`. Each of these has
a type and can be specified when _constructing_ a new `ElementMetadata`
instance. This is in contrast to an _end-user defined_ (or _ad-hoc_)
metadata field, one not known at "compile" time and added at the
discretion of an end-user to suit the purposes of their application.
An ad-hoc field can only be added by _assignment_ on an already
constructed instance.
### End-user ad-hoc metadata field behaviors
An ad-hoc field can be added to an `ElementMetadata` instance by
assignment:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
```
A field added in this way can be accessed by name:
```python
>>> metadata.coefficient
0.536
```
and that field will appear in the JSON/dict for that instance:
```python
>>> metadata = ElementMetadata()
>>> metadata.coefficient = 0.536
>>> metadata.to_dict()
{"coefficient": 0.536}
```
However, accessing a "user-defined" value that has _not_ been assigned
on that instance raises `AttributeError`:
```python
>>> metadata.coeffcient # -- misspelled "coefficient" --
AttributeError: 'ElementMetadata' object has no attribute 'coeffcient'
```
This makes "tagging" a metadata item with a value very convenient, but
entails the proviso that if an end-user wants to add a metadata field to
_some_ elements and not others (sparse population), AND they want to
access that field by name on ANY element and receive `None` where it has
not been assigned, they will need to use an expression like this:
```python
coefficient = metadata.coefficient if hasattr(metadata, "coefficient") else None
```
### Implementation Notes
- **ad-hoc metadata fields** are discarded during consolidation (for
chunking) because we don't have a consolidation strategy defined for
those. We could consider using a default consolidation strategy like
`FIRST` or possibly allow a user to register a strategy (although that
gets hairy in non-private and multiple-memory-space situations.)
- ad-hoc metadata fields **cannot start with an underscore**.
- We have no way to distinguish an ad-hoc field from any "noise" fields
that might appear in a JSON/dict loaded using `.from_dict()`, so unlike
the original (which only loaded known-fields), we'll rehydrate anything
that we find there.
- No real type-safety is possible on ad-hoc fields but the type-checker
does not complain because the type of all ad-hoc fields is `Any` (which
is the best available behavior in my view).
- We may want to consider whether end-users should be able to add ad-hoc
fields to "sub" metadata objects too, like `DataSourceMetadata` and
conceivably `CoordinatesMetadata` (although I'm not immediately seeing a
use-case for the second one).
2023-11-15 13:22:15 -08:00
|
|
|
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
|
2024-03-14 14:31:58 -07:00
|
|
|
|
|
|
|
|
2024-12-09 10:57:22 -08:00
|
|
|
def input_path(rel_path: str) -> str:
|
|
|
|
"""Resolve the absolute-path to `rel_path` in the testfiles directory."""
|
|
|
|
testfiles_dir = pathlib.Path(__file__).parent / "testfiles"
|
|
|
|
file_path = testfiles_dir / rel_path
|
|
|
|
return str(file_path.resolve())
|
|
|
|
|
|
|
|
|
2024-03-14 14:31:58 -07:00
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
# 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,
|
2024-07-02 11:42:03 -05:00
|
|
|
) -> Mock:
|
2024-03-14 14:31:58 -07:00
|
|
|
"""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
|
2024-05-16 15:14:02 -07:00
|
|
|
) -> Mock:
|
2024-03-14 14:31:58 -07:00
|
|
|
"""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()
|
|
|
|
|
|
|
|
|
2024-07-02 11:42:03 -05:00
|
|
|
def initializer_mock(
|
|
|
|
request: FixtureRequest, cls: type, autospec: bool = True, **kwargs: Any
|
|
|
|
) -> Mock:
|
2024-03-14 14:31:58 -07:00
|
|
|
"""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,
|
2024-07-02 11:42:03 -05:00
|
|
|
) -> Mock:
|
2024-03-14 14:31:58 -07:00
|
|
|
"""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)
|
|
|
|
|
|
|
|
|
2024-07-02 11:42:03 -05:00
|
|
|
def loose_mock(request: FixtureRequest, name: str | None = None, **kwargs: Any) -> Mock:
|
2024-03-14 14:31:58 -07:00
|
|
|
"""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,
|
2024-07-02 11:42:03 -05:00
|
|
|
) -> Mock:
|
2024-03-14 14:31:58 -07:00
|
|
|
"""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)
|
2024-08-27 18:02:24 -04:00
|
|
|
return _patch.start()
|
|
|
|
|
|
|
|
|
|
|
|
def stdlib_fn_mock(
|
|
|
|
request: FixtureRequest,
|
|
|
|
std_mod: types.ModuleType,
|
|
|
|
fn_name: str,
|
|
|
|
autospec: bool = True,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> Mock:
|
|
|
|
"""Return mock for function `fn_name` on `std_mod`.
|
|
|
|
|
|
|
|
The patch is reversed after pytest uses it.
|
|
|
|
"""
|
|
|
|
_patch = patch.object(std_mod, fn_name, autospec=autospec, **kwargs)
|
|
|
|
request.addfinalizer(_patch.stop)
|
2024-03-14 14:31:58 -07:00
|
|
|
return _patch.start()
|
|
|
|
|
|
|
|
|
2024-07-02 11:42:03 -05:00
|
|
|
def open_mock(request: FixtureRequest, module_name: str, **kwargs: Any) -> Mock:
|
2024-03-14 14:31:58 -07:00
|
|
|
"""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()
|
|
|
|
|
|
|
|
|
2024-07-02 11:42:03 -05:00
|
|
|
def var_mock(request: FixtureRequest, q_var_name: str, **kwargs: Any) -> Mock:
|
2024-03-14 14:31:58 -07:00
|
|
|
"""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()
|
2025-02-05 21:27:18 -06:00
|
|
|
|
|
|
|
|
|
|
|
def find_text_in_elements(text: str, elements: List[Element]):
|
|
|
|
return any(el.text.find(text) != -1 for el in elements)
|