pytest-regtest 2.2.0a2__py2.py3-none-any.whl → 2.3.0__py2.py3-none-any.whl
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/__init__.py +47 -6
- pytest_regtest/numpy_handler.py +23 -3
- pytest_regtest/pandas_handler.py +8 -0
- pytest_regtest/polars_handler.py +114 -0
- pytest_regtest/pytest_regtest.py +284 -197
- pytest_regtest/register_third_party_handlers.py +21 -22
- pytest_regtest/snapshot_handler.py +130 -21
- pytest_regtest-2.3.0.dist-info/METADATA +111 -0
- pytest_regtest-2.3.0.dist-info/RECORD +13 -0
- pytest_regtest-2.2.0a2.dist-info/METADATA +0 -356
- pytest_regtest-2.2.0a2.dist-info/RECORD +0 -12
- {pytest_regtest-2.2.0a2.dist-info → pytest_regtest-2.3.0.dist-info}/WHEEL +0 -0
- {pytest_regtest-2.2.0a2.dist-info → pytest_regtest-2.3.0.dist-info}/entry_points.txt +0 -0
- {pytest_regtest-2.2.0a2.dist-info → pytest_regtest-2.3.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import numpy as np # noqa: F401
|
|
3
|
-
|
|
4
|
-
HAS_NUMPY = True
|
|
5
|
-
except ImportError:
|
|
6
|
-
HAS_NUMPY = False
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
import pandas as pd # noqa: F401
|
|
10
|
-
|
|
11
|
-
HAS_PANDAS = True
|
|
12
|
-
except ImportError:
|
|
13
|
-
HAS_PANDAS = False
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if HAS_PANDAS and HAS_NUMPY:
|
|
17
|
-
|
|
1
|
+
def register_pandas_handler():
|
|
18
2
|
def is_dataframe(obj):
|
|
19
3
|
try:
|
|
20
4
|
import pandas as pd
|
|
@@ -24,12 +8,12 @@ if HAS_PANDAS and HAS_NUMPY:
|
|
|
24
8
|
return False
|
|
25
9
|
|
|
26
10
|
from .pandas_handler import DataFrameHandler
|
|
27
|
-
from .snapshot_handler import
|
|
11
|
+
from .snapshot_handler import SnapshotHandlerRegistry
|
|
28
12
|
|
|
29
|
-
|
|
13
|
+
SnapshotHandlerRegistry.add_handler(is_dataframe, DataFrameHandler)
|
|
30
14
|
|
|
31
|
-
if HAS_NUMPY:
|
|
32
15
|
|
|
16
|
+
def register_numpy_handler():
|
|
33
17
|
def is_numpy(obj):
|
|
34
18
|
try:
|
|
35
19
|
import numpy as np
|
|
@@ -39,6 +23,21 @@ if HAS_NUMPY:
|
|
|
39
23
|
return False
|
|
40
24
|
|
|
41
25
|
from .numpy_handler import NumpyHandler
|
|
42
|
-
from .snapshot_handler import
|
|
26
|
+
from .snapshot_handler import SnapshotHandlerRegistry
|
|
27
|
+
|
|
28
|
+
SnapshotHandlerRegistry.add_handler(is_numpy, NumpyHandler)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_polars_handler():
|
|
32
|
+
def is_polars(obj):
|
|
33
|
+
try:
|
|
34
|
+
import polars as pl
|
|
35
|
+
|
|
36
|
+
return isinstance(obj, pl.DataFrame)
|
|
37
|
+
except ImportError:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
from .polars_handler import PolarsHandler
|
|
41
|
+
from .snapshot_handler import SnapshotHandlerRegistry
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
SnapshotHandlerRegistry.add_handler(is_polars, PolarsHandler)
|
|
@@ -1,42 +1,154 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import difflib
|
|
3
|
-
import io
|
|
4
3
|
import os
|
|
5
4
|
import pickle
|
|
5
|
+
import pprint
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
-
from
|
|
8
|
-
from typing import Any, Type, Union
|
|
7
|
+
from typing import Any, Union
|
|
9
8
|
|
|
10
9
|
import pytest
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class BaseSnapshotHandler(abc.ABC):
|
|
13
|
+
"""
|
|
14
|
+
Abstract base class to implement snapshot handlers.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@abc.abstractmethod
|
|
14
18
|
def __init__(
|
|
15
|
-
self,
|
|
16
19
|
handler_options: dict[str, Any],
|
|
17
|
-
pytest_config:
|
|
20
|
+
pytest_config: type[pytest.Config],
|
|
18
21
|
tw: int,
|
|
19
|
-
) -> None:
|
|
22
|
+
) -> None:
|
|
23
|
+
"""
|
|
24
|
+
This method is called within `snapshot.check` to configure the handler.
|
|
25
|
+
|
|
26
|
+
Parameters:
|
|
27
|
+
handler_options: Keyword arguments from `shapshot.check` call
|
|
28
|
+
pytest_config: `pytest` config object.
|
|
29
|
+
tw: Terminal width.
|
|
30
|
+
"""
|
|
20
31
|
|
|
21
32
|
@abc.abstractmethod
|
|
22
|
-
def save(self, folder: Union[str, os.PathLike], obj: Any) -> None:
|
|
33
|
+
def save(self, folder: Union[str, os.PathLike], obj: Any) -> None:
|
|
34
|
+
"""
|
|
35
|
+
This method is called when a snapshot is reset, and a subclass must store the
|
|
36
|
+
given object in the given folder. How this folder is managed internally is
|
|
37
|
+
entirely up to the snapshot handler. It can be a single file, or a complex
|
|
38
|
+
structure of multiple files and sub-folders.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
folder: Path to the folder. The folder exists already when this
|
|
43
|
+
method is called.
|
|
44
|
+
obj: The object from the `snapshot.check` call.
|
|
45
|
+
"""
|
|
23
46
|
|
|
24
47
|
@abc.abstractmethod
|
|
25
|
-
def load(self, folder: Union[str, os.PathLike]) -> Any:
|
|
48
|
+
def load(self, folder: Union[str, os.PathLike]) -> Any:
|
|
49
|
+
"""
|
|
50
|
+
This method loads an existing snapshot from the given folder and must
|
|
51
|
+
be consistent with the `save` method.
|
|
52
|
+
|
|
53
|
+
Parameters:
|
|
54
|
+
folder: Path to the folder. The folder exists already when this
|
|
55
|
+
method is called.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The loaded object.
|
|
59
|
+
"""
|
|
26
60
|
|
|
27
61
|
@abc.abstractmethod
|
|
28
|
-
def show(self, obj: Any) -> list[str]:
|
|
62
|
+
def show(self, obj: Any) -> list[str]:
|
|
63
|
+
"""
|
|
64
|
+
This method returns a line wise textual representation of the given object.
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
obj: The object the snapshot handler is managing.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of strings.
|
|
71
|
+
"""
|
|
29
72
|
|
|
30
73
|
@abc.abstractmethod
|
|
31
|
-
def compare(self, current_obj: Any, recorded_obj: Any) -> bool:
|
|
74
|
+
def compare(self, current_obj: Any, recorded_obj: Any) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
The method compares if the current object from the
|
|
77
|
+
`snapshot.check` call and the object loaded with the `load`
|
|
78
|
+
method are considered to be the same. If this returns `True`
|
|
79
|
+
the `snapshot.check` call will pass.
|
|
80
|
+
|
|
81
|
+
Parameters:
|
|
82
|
+
current_obj: The object the snapshot handler receiving from
|
|
83
|
+
`snapshot.check`.
|
|
84
|
+
recorded_obj: The object the snapshot handler is loading with
|
|
85
|
+
the `load` method.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Boolean value which decides is the `snapshot.check` call will pass.
|
|
89
|
+
"""
|
|
32
90
|
|
|
33
91
|
@abc.abstractmethod
|
|
34
92
|
def show_differences(
|
|
35
93
|
self, current_obj: Any, recorded_obj: Any, has_markup: bool
|
|
36
|
-
) -> list[str]:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
94
|
+
) -> list[str]:
|
|
95
|
+
"""
|
|
96
|
+
The method shows differences between the object from the
|
|
97
|
+
`snapshot.check` call and the object loaded with the `load`
|
|
98
|
+
method.
|
|
99
|
+
|
|
100
|
+
Parameters:
|
|
101
|
+
current_obj: The object the snapshot handler receiving from
|
|
102
|
+
`snapshot.check`.
|
|
103
|
+
recorded_obj: The object the snapshot handler is loading with
|
|
104
|
+
the `load` method.
|
|
105
|
+
has_markup: Indicates if the tests run within a terminal and
|
|
106
|
+
the method can use color output in case `has_markup` is
|
|
107
|
+
`True`.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of strings to describe the differences.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SnapshotHandlerRegistry:
|
|
115
|
+
"""
|
|
116
|
+
This class serves as a registry for the builtin snapshot handlers
|
|
117
|
+
and can be used to add more handlers for particular data types.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
_snapshot_handlers: list[tuple[Callable[[Any], bool]]] = []
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def add_handler(
|
|
124
|
+
clz,
|
|
125
|
+
check_function: Callable[[Any], bool],
|
|
126
|
+
handler_class: type[BaseSnapshotHandler],
|
|
127
|
+
):
|
|
128
|
+
"""Add a handler.
|
|
129
|
+
|
|
130
|
+
Parameters:
|
|
131
|
+
check_function: A function which takes an object and returns `True`
|
|
132
|
+
if the `handler_class` argument should be
|
|
133
|
+
used for the given object.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
clz._snapshot_handlers.append((check_function, handler_class))
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def get_handler(clz, obj: Any) -> BaseSnapshotHandler:
|
|
140
|
+
"""Find and initialize handler for the given object.
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
obj: The object for which we try to find a handler.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
An instance of the handler or `None` if no appropriate handler was found.
|
|
147
|
+
"""
|
|
148
|
+
for check_function, handler in clz._snapshot_handlers:
|
|
149
|
+
if check_function(obj):
|
|
150
|
+
return handler
|
|
151
|
+
return None
|
|
40
152
|
|
|
41
153
|
|
|
42
154
|
class PythonObjectHandler(BaseSnapshotHandler):
|
|
@@ -52,9 +164,7 @@ class PythonObjectHandler(BaseSnapshotHandler):
|
|
|
52
164
|
return pickle.load(fh)
|
|
53
165
|
|
|
54
166
|
def show(self, obj):
|
|
55
|
-
|
|
56
|
-
pprint(obj, stream=stream, compact=self.compact)
|
|
57
|
-
return stream.getvalue().splitlines()
|
|
167
|
+
return pprint.pformat(obj, compact=self.compact).splitlines()
|
|
58
168
|
|
|
59
169
|
def compare(self, current_obj, recorded_obj):
|
|
60
170
|
return recorded_obj == current_obj
|
|
@@ -71,9 +181,8 @@ class PythonObjectHandler(BaseSnapshotHandler):
|
|
|
71
181
|
)
|
|
72
182
|
|
|
73
183
|
|
|
74
|
-
|
|
75
|
-
(
|
|
184
|
+
def register_python_object_handler():
|
|
185
|
+
SnapshotHandlerRegistry.add_handler(
|
|
76
186
|
lambda obj: isinstance(obj, (int, float, str, list, tuple, dict, set)),
|
|
77
187
|
PythonObjectHandler,
|
|
78
|
-
)
|
|
79
|
-
)
|
|
188
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pytest-regtest
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: pytest plugin for snapshot regression testing
|
|
5
|
+
Project-URL: Source, https://gitlab.com/uweschmitt/pytest-regtest
|
|
6
|
+
Project-URL: Documentation, https://pytest-regtest.readthedocs.org
|
|
7
|
+
Author-email: Uwe Schmitt <uwe.schmitt@id.ethz.ch>
|
|
8
|
+
License: MIT License
|
|
9
|
+
License-File: LICENSE.txt
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Dist: pytest>7.2
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: black; extra == 'dev'
|
|
19
|
+
Requires-Dist: build; extra == 'dev'
|
|
20
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
21
|
+
Requires-Dist: jinja2-cli; extra == 'dev'
|
|
22
|
+
Requires-Dist: mistletoe; extra == 'dev'
|
|
23
|
+
Requires-Dist: mkdocs; extra == 'dev'
|
|
24
|
+
Requires-Dist: mkdocs-awesome-pages-plugin; extra == 'dev'
|
|
25
|
+
Requires-Dist: mkdocs-material; extra == 'dev'
|
|
26
|
+
Requires-Dist: mkdocstrings[python]; extra == 'dev'
|
|
27
|
+
Requires-Dist: numpy; extra == 'dev'
|
|
28
|
+
Requires-Dist: numpy>=2.1.1; extra == 'dev'
|
|
29
|
+
Requires-Dist: pandas; extra == 'dev'
|
|
30
|
+
Requires-Dist: pandas>=2.2.3; extra == 'dev'
|
|
31
|
+
Requires-Dist: polars>=1.9.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
35
|
+
Requires-Dist: twine; extra == 'dev'
|
|
36
|
+
Requires-Dist: wheel; extra == 'dev'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
The full documentatin for this package are available at
|
|
44
|
+
https://pytest-regtest.readthedocs.org
|
|
45
|
+
|
|
46
|
+
# About
|
|
47
|
+
|
|
48
|
+
## Introduction
|
|
49
|
+
|
|
50
|
+
`pytest-regtest` is a plugin for [pytest](https://pytest.org) to implement
|
|
51
|
+
**regression testing**.
|
|
52
|
+
|
|
53
|
+
Unlike [functional testing](https://en.wikipedia.org/wiki/Functional_testing),
|
|
54
|
+
[regression testing](https://en.wikipedia.org/wiki/Regression_testing)
|
|
55
|
+
does not test whether the software produces the correct
|
|
56
|
+
results, but whether it behaves as it did before changes were introduced.
|
|
57
|
+
|
|
58
|
+
More specifically, `pytest-regtest` provides **snapshot testing**, which
|
|
59
|
+
implements regression testing by recording data within a test function
|
|
60
|
+
and comparing this recorded output to a previously recorded reference
|
|
61
|
+
output.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
To install and activate this plugin execute:
|
|
67
|
+
|
|
68
|
+
$ pip install pytest-regtest
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
!!! note
|
|
72
|
+
|
|
73
|
+
`pyteset-regtest` offers some functionalities which are tailored
|
|
74
|
+
for `NumPy`, `pandas` and `polars`. These depencies are not
|
|
75
|
+
installed if you install `pytest-regtest`. In case you are using
|
|
76
|
+
e.g. `NumPy` snapshots, we assume that your productive code (the
|
|
77
|
+
code under test) uses `NumPy` and thus should be part of the setup
|
|
78
|
+
of your project.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## Use case 1: Changing code with no or little testing setup yet
|
|
82
|
+
If you're working with code that has little or no unit testing, you
|
|
83
|
+
can use regression testing to ensure that your changes don't break or
|
|
84
|
+
alter previous results.
|
|
85
|
+
|
|
86
|
+
**Example**:
|
|
87
|
+
This can be useful when working with data analysis scripts, which often
|
|
88
|
+
start as one long script and then are restructured into different
|
|
89
|
+
functions as they evolve.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## Use case 2: Testing complex data
|
|
93
|
+
If a unit tests contains many `assert` statements to check a complex
|
|
94
|
+
data structure you can use regression tests instead.
|
|
95
|
+
|
|
96
|
+
**Example**: To test code which ingests data into a database one can
|
|
97
|
+
use regression tests on textual database dumps.
|
|
98
|
+
|
|
99
|
+
## Use case 3: Testing NumPy arrays or pandas data frames
|
|
100
|
+
|
|
101
|
+
If your code generates numerical results, such as `NumPy` arrays,
|
|
102
|
+
`pandas` or `polars` data frames, you can use `pytest-regtest` to simply record such
|
|
103
|
+
results and test them later, taking into account relative and absolute
|
|
104
|
+
tolerances.
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
**Example**:
|
|
108
|
+
A function creates a 10 x 10 matrix. Either you have to write 100
|
|
109
|
+
assert statements or you use summary statistics to test your result.
|
|
110
|
+
In both cases, you may get little debugging information if a test
|
|
111
|
+
fails.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pytest_regtest/__init__.py,sha256=q3wEEi3ZZXaN5CXc0jKCeupbAl73t64Fz1pFNcYYPYo,2536
|
|
2
|
+
pytest_regtest/numpy_handler.py,sha256=hKrVY9dId-45rUR_83w9jbUWgUhd0ABk8uLbOhgm0IM,7173
|
|
3
|
+
pytest_regtest/pandas_handler.py,sha256=7vKaHyODcKMltgn9MbbbL1FsOBQK994dEY5fVlDoQ1g,4528
|
|
4
|
+
pytest_regtest/polars_handler.py,sha256=B1CFNz0L96W8ktFMAT_j13q7nJZVN8ug402ILJjuBZA,3771
|
|
5
|
+
pytest_regtest/pytest_regtest.py,sha256=umtd97AIpY4FpaYJCd741mTlJbrToWkUfrkoKCvHDAE,22135
|
|
6
|
+
pytest_regtest/register_third_party_handlers.py,sha256=mfmcyeMKuxFykkKLO7T1Tz5RPitn1pKLYRM7B9lA1Kg,1132
|
|
7
|
+
pytest_regtest/snapshot_handler.py,sha256=5q1oMisifbQ_i8LCSPsD1OxPVEHVUIWwZeEDcZcBNDI,6036
|
|
8
|
+
pytest_regtest/utils.py,sha256=2jYTlV_qL5hH6FCeg7T1HJJvKual-Kux2scJ9_aB1lY,811
|
|
9
|
+
pytest_regtest-2.3.0.dist-info/METADATA,sha256=AJQuXnr5Dqc2mcGI5zdXdOoxRs227YIa7owdSFY2SQU,4097
|
|
10
|
+
pytest_regtest-2.3.0.dist-info/WHEEL,sha256=fl6v0VwpzfGBVsGtkAkhILUlJxROXbA3HvRL6Fe3140,105
|
|
11
|
+
pytest_regtest-2.3.0.dist-info/entry_points.txt,sha256=4VuIhXeMGhDo0ATbaUfyjND0atofmZjV_P-o6_uEk2s,36
|
|
12
|
+
pytest_regtest-2.3.0.dist-info/licenses/LICENSE.txt,sha256=Tue36uAzpW79-9WAqzkwPhsDDVd1X-VWUmdZ0MfGYvk,1068
|
|
13
|
+
pytest_regtest-2.3.0.dist-info/RECORD,,
|