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.
@@ -1,20 +1,4 @@
1
- try:
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 snapshot_handlers
11
+ from .snapshot_handler import SnapshotHandlerRegistry
28
12
 
29
- snapshot_handlers.append((is_dataframe, DataFrameHandler))
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 snapshot_handlers
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
- snapshot_handlers.append((is_numpy, NumpyHandler))
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 pprint import pprint
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: Type[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
- snapshot_handlers: list[tuple[Callable[[Any], bool]]] = []
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
- stream = io.StringIO()
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
- snapshot_handlers.append(
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
+ ![](https://gitlab.com/uweschmitt/pytest-regtest/badges/main/pipeline.svg)
40
+ ![](https://gitlab.com/uweschmitt/pytest-regtest/badges/main/coverage.svg?job=coverage)
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,,