pytest-regtest 2.1.1__tar.gz → 2.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_regtest-2.3.3/MANIFEST.in +2 -0
- pytest_regtest-2.3.3/PKG-INFO +91 -0
- pytest_regtest-2.3.3/README.md +73 -0
- pytest_regtest-2.3.3/pyproject.toml +53 -0
- pytest_regtest-2.3.3/setup.cfg +4 -0
- pytest_regtest-2.3.3/src/pytest_regtest/__init__.py +108 -0
- pytest_regtest-2.3.3/src/pytest_regtest/numpy_handler.py +216 -0
- pytest_regtest-2.3.3/src/pytest_regtest/pandas_handler.py +143 -0
- pytest_regtest-2.3.3/src/pytest_regtest/polars_handler.py +114 -0
- pytest_regtest-2.3.3/src/pytest_regtest/pytest_regtest.py +646 -0
- pytest_regtest-2.3.3/src/pytest_regtest/register_third_party_handlers.py +43 -0
- pytest_regtest-2.3.3/src/pytest_regtest/snapshot_handler.py +188 -0
- pytest_regtest-2.3.3/src/pytest_regtest/utils.py +28 -0
- pytest_regtest-2.3.3/src/pytest_regtest.egg-info/PKG-INFO +91 -0
- pytest_regtest-2.3.3/src/pytest_regtest.egg-info/SOURCES.txt +27 -0
- pytest_regtest-2.3.3/src/pytest_regtest.egg-info/dependency_links.txt +1 -0
- pytest_regtest-2.3.3/src/pytest_regtest.egg-info/entry_points.txt +2 -0
- pytest_regtest-2.3.3/src/pytest_regtest.egg-info/requires.txt +1 -0
- pytest_regtest-2.3.3/src/pytest_regtest.egg-info/top_level.txt +1 -0
- pytest_regtest-2.3.3/tests/conftest.py +28 -0
- pytest_regtest-2.3.3/tests/test_cli.py +69 -0
- pytest_regtest-2.1.1/tests/test_plugin.py → pytest_regtest-2.3.3/tests/test_regtest.py +176 -63
- pytest_regtest-2.3.3/tests/test_snapshot.py +37 -0
- pytest_regtest-2.3.3/tests/test_snapshot_numpy.py +510 -0
- pytest_regtest-2.3.3/tests/test_snapshot_pandas.py +224 -0
- pytest_regtest-2.3.3/tests/test_snapshot_polars.py +214 -0
- pytest_regtest-2.3.3/tests/test_snapshot_python_types.py +109 -0
- pytest_regtest-2.3.3/tests/test_utils.py +21 -0
- pytest_regtest-2.1.1/.gitignore +0 -13
- pytest_regtest-2.1.1/PKG-INFO +0 -348
- pytest_regtest-2.1.1/docs/index.md +0 -325
- pytest_regtest-2.1.1/pyproject.toml +0 -58
- pytest_regtest-2.1.1/pytest_regtest/__init__.py +0 -14
- pytest_regtest-2.1.1/pytest_regtest/pytest_regtest.py +0 -419
- pytest_regtest-2.1.1/tests/conftest.py +0 -1
- {pytest_regtest-2.1.1 → pytest_regtest-2.3.3}/LICENSE.txt +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pytest-regtest
|
|
3
|
+
Version: 2.3.3
|
|
4
|
+
Summary: pytest plugin for snapshot regression testing
|
|
5
|
+
Author-email: Uwe Schmitt <uwe.schmitt@id.ethz.ch>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Source, https://gitlab.com/uweschmitt/pytest-regtest
|
|
8
|
+
Project-URL: Documentation, https://pytest-regtest.readthedocs.org
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE.txt
|
|
17
|
+
Requires-Dist: pytest>7.2
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
The full documentation for this package are available at
|
|
24
|
+
https://pytest-regtest.readthedocs.org
|
|
25
|
+
|
|
26
|
+
# About
|
|
27
|
+
|
|
28
|
+
## Introduction
|
|
29
|
+
|
|
30
|
+
`pytest-regtest` is a plugin for [pytest](https://pytest.org) to implement
|
|
31
|
+
**regression testing**.
|
|
32
|
+
|
|
33
|
+
Unlike [functional testing](https://en.wikipedia.org/wiki/Functional_testing),
|
|
34
|
+
[regression testing](https://en.wikipedia.org/wiki/Regression_testing)
|
|
35
|
+
does not test whether the software produces the correct
|
|
36
|
+
results, but whether it behaves as it did before changes were introduced.
|
|
37
|
+
|
|
38
|
+
More specifically, `pytest-regtest` provides **snapshot testing**, which
|
|
39
|
+
implements regression testing by recording data within a test function
|
|
40
|
+
and comparing this recorded output to a previously recorded reference
|
|
41
|
+
output.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
To install and activate this plugin execute:
|
|
47
|
+
|
|
48
|
+
$ pip install pytest-regtest
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
!!! note
|
|
52
|
+
|
|
53
|
+
`pytest-regtest` provides some functionality specific to `NumPy`,
|
|
54
|
+
`pandas`, and `polars`. These dependencies are not installed when
|
|
55
|
+
you install `pytest-regtest`. For example, if you are using NumPy
|
|
56
|
+
snapshots, we assume that your production code (the code under
|
|
57
|
+
test) uses NumPy and therefore should be part of your project's
|
|
58
|
+
setup.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Use case 1: Changing code with no or little testing setup yet
|
|
62
|
+
If you're working with code that has little or no unit testing, you
|
|
63
|
+
can use regression testing to ensure that your changes don't break or
|
|
64
|
+
alter previous results.
|
|
65
|
+
|
|
66
|
+
**Example**:
|
|
67
|
+
This can be useful when working with data analysis scripts, which often
|
|
68
|
+
start as one long script and then are restructured into different
|
|
69
|
+
functions as they evolve.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Use case 2: Testing complex data
|
|
73
|
+
If a unit tests contains many `assert` statements to check a complex
|
|
74
|
+
data structure you can use regression tests instead.
|
|
75
|
+
|
|
76
|
+
**Example**: To test code which ingests data into a database one can
|
|
77
|
+
use regression tests on textual database dumps.
|
|
78
|
+
|
|
79
|
+
## Use case 3: Testing NumPy arrays or pandas data frames
|
|
80
|
+
|
|
81
|
+
If your code generates numerical results, such as `NumPy` arrays,
|
|
82
|
+
`pandas` or `polars` data frames, you can use `pytest-regtest` to simply record such
|
|
83
|
+
results and test them later, taking into account relative and absolute
|
|
84
|
+
tolerances.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
**Example**:
|
|
88
|
+
A function creates a 10 x 10 matrix. Either you have to write 100
|
|
89
|
+
assert statements or you use summary statistics to test your result.
|
|
90
|
+
In both cases, you may get little debugging information if a test
|
|
91
|
+
fails.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
The full documentation for this package are available at
|
|
6
|
+
https://pytest-regtest.readthedocs.org
|
|
7
|
+
|
|
8
|
+
# About
|
|
9
|
+
|
|
10
|
+
## Introduction
|
|
11
|
+
|
|
12
|
+
`pytest-regtest` is a plugin for [pytest](https://pytest.org) to implement
|
|
13
|
+
**regression testing**.
|
|
14
|
+
|
|
15
|
+
Unlike [functional testing](https://en.wikipedia.org/wiki/Functional_testing),
|
|
16
|
+
[regression testing](https://en.wikipedia.org/wiki/Regression_testing)
|
|
17
|
+
does not test whether the software produces the correct
|
|
18
|
+
results, but whether it behaves as it did before changes were introduced.
|
|
19
|
+
|
|
20
|
+
More specifically, `pytest-regtest` provides **snapshot testing**, which
|
|
21
|
+
implements regression testing by recording data within a test function
|
|
22
|
+
and comparing this recorded output to a previously recorded reference
|
|
23
|
+
output.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
To install and activate this plugin execute:
|
|
29
|
+
|
|
30
|
+
$ pip install pytest-regtest
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
!!! note
|
|
34
|
+
|
|
35
|
+
`pytest-regtest` provides some functionality specific to `NumPy`,
|
|
36
|
+
`pandas`, and `polars`. These dependencies are not installed when
|
|
37
|
+
you install `pytest-regtest`. For example, if you are using NumPy
|
|
38
|
+
snapshots, we assume that your production code (the code under
|
|
39
|
+
test) uses NumPy and therefore should be part of your project's
|
|
40
|
+
setup.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Use case 1: Changing code with no or little testing setup yet
|
|
44
|
+
If you're working with code that has little or no unit testing, you
|
|
45
|
+
can use regression testing to ensure that your changes don't break or
|
|
46
|
+
alter previous results.
|
|
47
|
+
|
|
48
|
+
**Example**:
|
|
49
|
+
This can be useful when working with data analysis scripts, which often
|
|
50
|
+
start as one long script and then are restructured into different
|
|
51
|
+
functions as they evolve.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Use case 2: Testing complex data
|
|
55
|
+
If a unit tests contains many `assert` statements to check a complex
|
|
56
|
+
data structure you can use regression tests instead.
|
|
57
|
+
|
|
58
|
+
**Example**: To test code which ingests data into a database one can
|
|
59
|
+
use regression tests on textual database dumps.
|
|
60
|
+
|
|
61
|
+
## Use case 3: Testing NumPy arrays or pandas data frames
|
|
62
|
+
|
|
63
|
+
If your code generates numerical results, such as `NumPy` arrays,
|
|
64
|
+
`pandas` or `polars` data frames, you can use `pytest-regtest` to simply record such
|
|
65
|
+
results and test them later, taking into account relative and absolute
|
|
66
|
+
tolerances.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
**Example**:
|
|
70
|
+
A function creates a 10 x 10 matrix. Either you have to write 100
|
|
71
|
+
assert statements or you use summary statistics to test your result.
|
|
72
|
+
In both cases, you may get little debugging information if a test
|
|
73
|
+
fails.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "pytest-regtest"
|
|
9
|
+
version = "2.3.3"
|
|
10
|
+
description = "pytest plugin for snapshot regression testing"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Uwe Schmitt", email = "uwe.schmitt@id.ethz.ch"}
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
license = {text = "MIT License"}
|
|
17
|
+
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"License :: OSI Approved :: MIT License"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"pytest>7.2",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Source = "https://gitlab.com/uweschmitt/pytest-regtest"
|
|
33
|
+
Documentation = "https://pytest-regtest.readthedocs.org"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
[project.entry-points.pytest11]
|
|
37
|
+
regtest = "pytest_regtest"
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 88
|
|
41
|
+
exclude = ["_regtest_output"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
ignore = ["E731", "E203"]
|
|
45
|
+
|
|
46
|
+
[tool.uv]
|
|
47
|
+
dev-dependencies = [
|
|
48
|
+
"twine", "build", "hatchling", "wheel", "pre-commit", "ruff", "black",
|
|
49
|
+
"pytest-cov", "numpy", "pandas", "mkdocs", "mkdocs-material", "mistletoe",
|
|
50
|
+
"mkdocs-awesome-pages-plugin", "jinja2-cli", "mkdocstrings[python]",
|
|
51
|
+
"numpy>=2", "pandas>=2", "polars>=1.9",
|
|
52
|
+
"md-transformer>=0.0.3"
|
|
53
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from importlib.metadata import version as _version
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from .pytest_regtest import clear_converters # noqa: F401
|
|
6
|
+
from .pytest_regtest import patch_terminal_size # noqa: F401
|
|
7
|
+
from .pytest_regtest import register_converter_post # noqa: F401
|
|
8
|
+
from .pytest_regtest import register_converter_pre # noqa: F401
|
|
9
|
+
from .pytest_regtest import (
|
|
10
|
+
PytestRegtestCommonHooks,
|
|
11
|
+
PytestRegtestPlugin,
|
|
12
|
+
RegtestStream,
|
|
13
|
+
Snapshot,
|
|
14
|
+
SnapshotPlugin,
|
|
15
|
+
)
|
|
16
|
+
from .register_third_party_handlers import (
|
|
17
|
+
register_numpy_handler,
|
|
18
|
+
register_pandas_handler,
|
|
19
|
+
register_polars_handler,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .snapshot_handler import register_python_object_handler
|
|
23
|
+
|
|
24
|
+
__version__ = _version(__package__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def pytest_addoption(parser):
|
|
28
|
+
"""Add options to control the timeout plugin"""
|
|
29
|
+
group = parser.getgroup("regtest", "regression test plugin")
|
|
30
|
+
group.addoption(
|
|
31
|
+
"--regtest-reset",
|
|
32
|
+
action="store_true",
|
|
33
|
+
help="do not run regtest but record current output",
|
|
34
|
+
)
|
|
35
|
+
group.addoption(
|
|
36
|
+
"--regtest-tee",
|
|
37
|
+
action="store_true",
|
|
38
|
+
default=False,
|
|
39
|
+
help="print recorded results to console too",
|
|
40
|
+
)
|
|
41
|
+
group.addoption(
|
|
42
|
+
"--regtest-consider-line-endings",
|
|
43
|
+
action="store_true",
|
|
44
|
+
default=False,
|
|
45
|
+
help="do not strip whitespaces at end of recorded lines",
|
|
46
|
+
)
|
|
47
|
+
group.addoption(
|
|
48
|
+
"--regtest-nodiff",
|
|
49
|
+
action="store_true",
|
|
50
|
+
default=False,
|
|
51
|
+
help="do not show diff output for failed regresson tests",
|
|
52
|
+
)
|
|
53
|
+
group.addoption(
|
|
54
|
+
"--regtest-disable-stdconv",
|
|
55
|
+
action="store_true",
|
|
56
|
+
default=False,
|
|
57
|
+
help=(
|
|
58
|
+
"do not apply standard output converters to clean up indeterministic output"
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def pytest_configure(config):
|
|
64
|
+
common = PytestRegtestCommonHooks()
|
|
65
|
+
config.pluginmanager.register(common)
|
|
66
|
+
config.pluginmanager.register(PytestRegtestPlugin(common))
|
|
67
|
+
config.pluginmanager.register(SnapshotPlugin(common))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def regtest(request):
|
|
72
|
+
yield RegtestStream(request)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def snapshot(request):
|
|
77
|
+
yield Snapshot(request)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.fixture
|
|
81
|
+
def regtest_all(regtest):
|
|
82
|
+
yield regtest
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
snapshot_all_output = regtest_all
|
|
86
|
+
|
|
87
|
+
register_python_object_handler()
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
import pandas # noqa: F401
|
|
91
|
+
|
|
92
|
+
register_pandas_handler()
|
|
93
|
+
except ImportError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
import numpy # noqa: F401
|
|
98
|
+
|
|
99
|
+
register_numpy_handler()
|
|
100
|
+
except ImportError:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
import polars # noqa: F401
|
|
105
|
+
|
|
106
|
+
register_polars_handler()
|
|
107
|
+
except ImportError:
|
|
108
|
+
pass
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import io
|
|
3
|
+
import os.path
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .snapshot_handler import BaseSnapshotHandler
|
|
9
|
+
from .utils import highlight_mismatches
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NumpyHandler(BaseSnapshotHandler):
|
|
13
|
+
def __init__(self, handler_options, pytest_config, tw):
|
|
14
|
+
self.atol = handler_options.get("atol", 0.0)
|
|
15
|
+
self.rtol = handler_options.get("rtol", 0.0)
|
|
16
|
+
self.equal_nan = handler_options.get("equal_nan", True)
|
|
17
|
+
if handler_options.get("print_options"):
|
|
18
|
+
warnings.warn(
|
|
19
|
+
"please use the numpy.printoptions context manager instead of"
|
|
20
|
+
" the print_options argument.",
|
|
21
|
+
DeprecationWarning,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
self.print_options = np.get_printoptions() | handler_options.get(
|
|
25
|
+
"print_options", {}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def _filename(self, folder):
|
|
29
|
+
return os.path.join(folder, "arrays.npy")
|
|
30
|
+
|
|
31
|
+
def save(self, folder, obj):
|
|
32
|
+
np.save(self._filename(folder), obj)
|
|
33
|
+
|
|
34
|
+
def load(self, folder):
|
|
35
|
+
return np.load(self._filename(folder))
|
|
36
|
+
|
|
37
|
+
def show(self, obj):
|
|
38
|
+
stream = io.StringIO()
|
|
39
|
+
with np.printoptions(**self.print_options):
|
|
40
|
+
print(obj, file=stream)
|
|
41
|
+
return stream.getvalue().splitlines()
|
|
42
|
+
|
|
43
|
+
def compare(self, current_obj, recorded_obj):
|
|
44
|
+
return (
|
|
45
|
+
isinstance(current_obj, np.ndarray)
|
|
46
|
+
and current_obj.shape == recorded_obj.shape
|
|
47
|
+
and current_obj.dtype == recorded_obj.dtype
|
|
48
|
+
and np.allclose(
|
|
49
|
+
recorded_obj,
|
|
50
|
+
current_obj,
|
|
51
|
+
atol=self.atol,
|
|
52
|
+
rtol=self.rtol,
|
|
53
|
+
equal_nan=self.equal_nan,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def show_differences(self, current_obj, recorded_obj, has_markup):
|
|
58
|
+
lines = []
|
|
59
|
+
|
|
60
|
+
if recorded_obj.dtype != current_obj.dtype:
|
|
61
|
+
lines.extend(
|
|
62
|
+
[
|
|
63
|
+
f"dtype mismatch: current dtype: {current_obj.dtype}",
|
|
64
|
+
f" recorded dtype: {recorded_obj.dtype}",
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
recorded_as_text = self.show(recorded_obj)
|
|
69
|
+
current_as_text = self.show(current_obj)
|
|
70
|
+
|
|
71
|
+
if recorded_obj.shape == current_obj.shape:
|
|
72
|
+
if np.allclose(current_obj, recorded_obj, rtol=self.rtol, atol=self.atol):
|
|
73
|
+
return lines or None
|
|
74
|
+
|
|
75
|
+
lines.extend(self.error_diagnostics(recorded_obj, current_obj))
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
lines.extend(
|
|
79
|
+
[
|
|
80
|
+
f"shape mismatch: current shape: {current_obj.shape}",
|
|
81
|
+
f" recorded shape: {recorded_obj.shape}",
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if recorded_obj.ndim > 2:
|
|
86
|
+
return lines
|
|
87
|
+
|
|
88
|
+
if recorded_obj.ndim == 1:
|
|
89
|
+
diff_lines = list(
|
|
90
|
+
difflib.unified_diff(
|
|
91
|
+
current_as_text,
|
|
92
|
+
recorded_as_text,
|
|
93
|
+
"current",
|
|
94
|
+
"expected",
|
|
95
|
+
lineterm="",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
lines.append("")
|
|
99
|
+
lines.extend(diff_lines)
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
diff_lines = self.error_diagnostics_2d_linewise(
|
|
103
|
+
current_obj,
|
|
104
|
+
current_as_text,
|
|
105
|
+
recorded_obj,
|
|
106
|
+
recorded_as_text,
|
|
107
|
+
has_markup,
|
|
108
|
+
)
|
|
109
|
+
lines.extend(diff_lines)
|
|
110
|
+
|
|
111
|
+
if not diff_lines:
|
|
112
|
+
lines.append("diff is empty, you may want to change the print options")
|
|
113
|
+
|
|
114
|
+
return lines
|
|
115
|
+
|
|
116
|
+
def error_diagnostics(self, recorded_obj, current_obj):
|
|
117
|
+
with warnings.catch_warnings():
|
|
118
|
+
warnings.simplefilter("ignore", RuntimeWarning)
|
|
119
|
+
rel_err = np.abs(current_obj - recorded_obj) / recorded_obj
|
|
120
|
+
rel_err[(recorded_obj == 0) * (current_obj == recorded_obj)] = 0.0
|
|
121
|
+
rel_err_max_1 = np.max(rel_err)
|
|
122
|
+
rel_err_max_2 = np.max(rel_err[recorded_obj != 0])
|
|
123
|
+
|
|
124
|
+
abs_err = np.abs(current_obj - recorded_obj)
|
|
125
|
+
abs_err_max = np.max(abs_err)
|
|
126
|
+
|
|
127
|
+
lines = []
|
|
128
|
+
|
|
129
|
+
if rel_err_max_1 == rel_err_max_2:
|
|
130
|
+
lines.append(f"max relative deviation: {rel_err_max_1:e}")
|
|
131
|
+
else:
|
|
132
|
+
lines.append(f"max relative deviation: {rel_err_max_1:e}")
|
|
133
|
+
lines.append(f"max relative deviation except inf: {rel_err_max_2:e}")
|
|
134
|
+
|
|
135
|
+
lines.append(f"max absolute deviation: {abs_err_max:e}")
|
|
136
|
+
|
|
137
|
+
n_diff = np.sum(
|
|
138
|
+
np.logical_not(
|
|
139
|
+
np.isclose(current_obj, recorded_obj, rtol=self.rtol, atol=self.atol)
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
lines.append(
|
|
144
|
+
f"both arrays differ in {n_diff} out of {np.prod(recorded_obj.shape)}"
|
|
145
|
+
" entries"
|
|
146
|
+
)
|
|
147
|
+
lines.append(
|
|
148
|
+
f"up to given precision settings rtol={self.rtol:e} and"
|
|
149
|
+
f" atol={self.atol:e}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return lines
|
|
153
|
+
|
|
154
|
+
def error_diagnostics_2d_linewise(
|
|
155
|
+
self, current_obj, current_as_text, recorded_obj, recorded_as_text, has_markup
|
|
156
|
+
):
|
|
157
|
+
sub_diff = []
|
|
158
|
+
|
|
159
|
+
for i, (l1, l2, r1, r2) in enumerate(
|
|
160
|
+
zip(current_as_text, recorded_as_text, current_obj, recorded_obj)
|
|
161
|
+
):
|
|
162
|
+
if r1.shape == r2.shape and np.allclose(
|
|
163
|
+
r1, r2, rtol=self.rtol, atol=self.atol
|
|
164
|
+
):
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
if r1.shape == r2.shape:
|
|
168
|
+
# enforces more uniform formatting of both lines:
|
|
169
|
+
rows_together = np.vstack((r1, r2))
|
|
170
|
+
lines_together = self.show(rows_together)
|
|
171
|
+
line_diff = list(
|
|
172
|
+
difflib.unified_diff(
|
|
173
|
+
[lines_together[0][1:].strip()],
|
|
174
|
+
[lines_together[1][:-1].strip()],
|
|
175
|
+
"current",
|
|
176
|
+
"expected",
|
|
177
|
+
lineterm="",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
row_1 = self.show(r1)
|
|
182
|
+
row_2 = self.show(r2)
|
|
183
|
+
line_diff = list(
|
|
184
|
+
difflib.unified_diff(
|
|
185
|
+
row_1,
|
|
186
|
+
row_2,
|
|
187
|
+
"current",
|
|
188
|
+
"expected",
|
|
189
|
+
lineterm="",
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if line_diff:
|
|
194
|
+
if not sub_diff:
|
|
195
|
+
sub_diff = line_diff[:2]
|
|
196
|
+
|
|
197
|
+
l1, l2 = line_diff[-2], line_diff[-1]
|
|
198
|
+
if has_markup:
|
|
199
|
+
l1, l2 = highlight_mismatches(l1, l2)
|
|
200
|
+
|
|
201
|
+
sub_diff.append(f"row {i:3d}: {l1}")
|
|
202
|
+
sub_diff.append(f" {l2}")
|
|
203
|
+
|
|
204
|
+
missing = len(current_as_text) - len(recorded_as_text)
|
|
205
|
+
if missing > 0:
|
|
206
|
+
for i, row in enumerate(current_as_text[-missing:], len(recorded_as_text)):
|
|
207
|
+
# remove duplicate brackets
|
|
208
|
+
row = row.rstrip("]") + "]"
|
|
209
|
+
sub_diff.append(f"row {i:3d}: -{row.lstrip()}")
|
|
210
|
+
if missing < 0:
|
|
211
|
+
for i, row in enumerate(recorded_as_text[missing:], len(current_as_text)):
|
|
212
|
+
# remove duplicate brackets
|
|
213
|
+
row = row.rstrip("]") + "]"
|
|
214
|
+
sub_diff.append(f"row {i:3d}: +{row.lstrip()}")
|
|
215
|
+
|
|
216
|
+
return sub_diff
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import io
|
|
3
|
+
import os.path
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .snapshot_handler import BaseSnapshotHandler
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataFrameHandler(BaseSnapshotHandler):
|
|
13
|
+
def __init__(self, handler_options, pytest_config, tw):
|
|
14
|
+
if handler_options.get("display_options"):
|
|
15
|
+
warnings.warn(
|
|
16
|
+
"please use the 'pandas.option_context' context manager instead of"
|
|
17
|
+
" the display_options argument.",
|
|
18
|
+
DeprecationWarning,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# default contains a few nested dicts and we flatten those, e.g.
|
|
22
|
+
# { "html": {"border": 1} } -> { "html.border": 1 }
|
|
23
|
+
default = list(pd.options.display.d.items())
|
|
24
|
+
default_flattened = {}
|
|
25
|
+
for k, v in default:
|
|
26
|
+
if isinstance(v, dict):
|
|
27
|
+
for k0, v0 in v.items():
|
|
28
|
+
default_flattened[f"{k}.{k0}"] = v0
|
|
29
|
+
else:
|
|
30
|
+
default_flattened[k] = v
|
|
31
|
+
|
|
32
|
+
# overwrite with user settings:
|
|
33
|
+
items = (default_flattened | handler_options.get("display_options", {})).items()
|
|
34
|
+
|
|
35
|
+
# flatten items as required by pandas.option_context:
|
|
36
|
+
self.display_options_flat = [
|
|
37
|
+
entry for item in items for entry in (f"display.{item[0]}", item[1])
|
|
38
|
+
]
|
|
39
|
+
self.atol = handler_options.get("atol", 0.0)
|
|
40
|
+
self.rtol = handler_options.get("rtol", 0.0)
|
|
41
|
+
|
|
42
|
+
def _filename(self, folder):
|
|
43
|
+
return os.path.join(folder, "dataframe.pkl")
|
|
44
|
+
|
|
45
|
+
def save(self, folder, obj):
|
|
46
|
+
obj.to_pickle(self._filename(folder), compression="gzip")
|
|
47
|
+
|
|
48
|
+
def load(self, folder):
|
|
49
|
+
return pd.read_pickle(self._filename(folder), compression="gzip")
|
|
50
|
+
|
|
51
|
+
def show(self, obj):
|
|
52
|
+
stream = io.StringIO()
|
|
53
|
+
with pd.option_context(*self.display_options_flat):
|
|
54
|
+
print(obj, file=stream)
|
|
55
|
+
return stream.getvalue().splitlines()
|
|
56
|
+
|
|
57
|
+
def compare(self, current, recorded):
|
|
58
|
+
missing = set(
|
|
59
|
+
n
|
|
60
|
+
for (n, t) in set(zip(recorded.columns, recorded.dtypes))
|
|
61
|
+
^ set(zip(current.columns, current.dtypes))
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if missing:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
common = set(
|
|
68
|
+
n
|
|
69
|
+
for (n, t) in set(zip(recorded.columns, recorded.dtypes))
|
|
70
|
+
& set(zip(current.columns, current.dtypes))
|
|
71
|
+
)
|
|
72
|
+
current_reduced = current[[n for n in current.columns if n in common]]
|
|
73
|
+
recorded_reduced = recorded[[n for n in recorded.columns if n in common]]
|
|
74
|
+
|
|
75
|
+
def extract(df, selector):
|
|
76
|
+
return df[[n for (n, t) in zip(df.columns, df.dtypes) if selector(t)]]
|
|
77
|
+
|
|
78
|
+
current_reduced_floats = extract(
|
|
79
|
+
current_reduced, lambda t: t.type is np.float64
|
|
80
|
+
).to_numpy()
|
|
81
|
+
|
|
82
|
+
current_reduced_other = extract(
|
|
83
|
+
current_reduced, lambda t: t.type is not np.float64
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
recorded_reduced_floats = extract(
|
|
87
|
+
recorded_reduced, lambda t: t.type is np.float64
|
|
88
|
+
).to_numpy()
|
|
89
|
+
|
|
90
|
+
recorded_reduced_other = extract(
|
|
91
|
+
recorded_reduced, lambda t: t.type is not np.float64
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return np.allclose(
|
|
95
|
+
current_reduced_floats,
|
|
96
|
+
recorded_reduced_floats,
|
|
97
|
+
atol=self.atol,
|
|
98
|
+
rtol=self.rtol,
|
|
99
|
+
equal_nan=True,
|
|
100
|
+
) and (current_reduced_other == recorded_reduced_other).all(axis=None)
|
|
101
|
+
|
|
102
|
+
def show_differences(self, current, recorded, has_markup):
|
|
103
|
+
lines = []
|
|
104
|
+
|
|
105
|
+
stream = io.StringIO()
|
|
106
|
+
current.info(buf=stream, verbose=True, memory_usage=False)
|
|
107
|
+
current_info = stream.getvalue().splitlines()[2:][:-1]
|
|
108
|
+
|
|
109
|
+
stream = io.StringIO()
|
|
110
|
+
recorded.info(buf=stream, verbose=True, memory_usage=False)
|
|
111
|
+
recorded_info = stream.getvalue().splitlines()[2:][:-1]
|
|
112
|
+
|
|
113
|
+
info_diff = list(
|
|
114
|
+
difflib.unified_diff(
|
|
115
|
+
current_info,
|
|
116
|
+
recorded_info,
|
|
117
|
+
"current",
|
|
118
|
+
"expected",
|
|
119
|
+
lineterm="",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
lines.extend(info_diff)
|
|
123
|
+
|
|
124
|
+
recorded_as_text = self.show(recorded)
|
|
125
|
+
current_as_text = self.show(current)
|
|
126
|
+
|
|
127
|
+
diffs = list(
|
|
128
|
+
difflib.unified_diff(
|
|
129
|
+
current_as_text,
|
|
130
|
+
recorded_as_text,
|
|
131
|
+
"current",
|
|
132
|
+
"expected",
|
|
133
|
+
lineterm="",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
lines.append("")
|
|
138
|
+
if diffs:
|
|
139
|
+
lines.extend(diffs)
|
|
140
|
+
else:
|
|
141
|
+
lines.append("diff is empty, you may want to change the print options")
|
|
142
|
+
|
|
143
|
+
return lines
|