scikit-base 0.4.6__py3-none-any.whl → 0.5.1__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.
- docs/source/conf.py +299 -299
- {scikit_base-0.4.6.dist-info → scikit_base-0.5.1.dist-info}/LICENSE +29 -29
- {scikit_base-0.4.6.dist-info → scikit_base-0.5.1.dist-info}/METADATA +160 -159
- scikit_base-0.5.1.dist-info/RECORD +58 -0
- {scikit_base-0.4.6.dist-info → scikit_base-0.5.1.dist-info}/WHEEL +1 -1
- scikit_base-0.5.1.dist-info/top_level.txt +5 -0
- {scikit_base-0.4.6.dist-info → scikit_base-0.5.1.dist-info}/zip-safe +1 -1
- skbase/__init__.py +14 -14
- skbase/_exceptions.py +31 -31
- skbase/_nopytest_tests.py +35 -35
- skbase/base/__init__.py +20 -20
- skbase/base/_base.py +1249 -1249
- skbase/base/_meta.py +883 -871
- skbase/base/_pretty_printing/__init__.py +11 -11
- skbase/base/_pretty_printing/_object_html_repr.py +392 -392
- skbase/base/_pretty_printing/_pprint.py +412 -412
- skbase/base/_tagmanager.py +217 -217
- skbase/lookup/__init__.py +31 -31
- skbase/lookup/_lookup.py +1009 -1009
- skbase/lookup/tests/__init__.py +2 -2
- skbase/lookup/tests/test_lookup.py +991 -991
- skbase/testing/__init__.py +12 -12
- skbase/testing/test_all_objects.py +852 -856
- skbase/testing/utils/__init__.py +5 -5
- skbase/testing/utils/_conditional_fixtures.py +209 -209
- skbase/testing/utils/_dependencies.py +15 -15
- skbase/testing/utils/deep_equals.py +15 -15
- skbase/testing/utils/inspect.py +30 -30
- skbase/testing/utils/tests/__init__.py +2 -2
- skbase/testing/utils/tests/test_check_dependencies.py +49 -49
- skbase/testing/utils/tests/test_deep_equals.py +66 -66
- skbase/tests/__init__.py +2 -2
- skbase/tests/conftest.py +273 -273
- skbase/tests/mock_package/__init__.py +5 -5
- skbase/tests/mock_package/test_mock_package.py +74 -74
- skbase/tests/test_base.py +1202 -1202
- skbase/tests/test_baseestimator.py +130 -130
- skbase/tests/test_exceptions.py +23 -23
- skbase/tests/test_meta.py +170 -131
- skbase/utils/__init__.py +21 -21
- skbase/utils/_check.py +53 -53
- skbase/utils/_iter.py +238 -238
- skbase/utils/_nested_iter.py +180 -180
- skbase/utils/_utils.py +91 -91
- skbase/utils/deep_equals.py +358 -358
- skbase/utils/dependencies/__init__.py +11 -11
- skbase/utils/dependencies/_dependencies.py +253 -253
- skbase/utils/tests/__init__.py +4 -4
- skbase/utils/tests/test_check.py +24 -24
- skbase/utils/tests/test_iter.py +127 -127
- skbase/utils/tests/test_nested_iter.py +84 -84
- skbase/utils/tests/test_utils.py +37 -37
- skbase/validate/__init__.py +22 -22
- skbase/validate/_named_objects.py +403 -403
- skbase/validate/_types.py +345 -345
- skbase/validate/tests/__init__.py +2 -2
- skbase/validate/tests/test_iterable_named_objects.py +200 -200
- skbase/validate/tests/test_type_validations.py +370 -370
- scikit_base-0.4.6.dist-info/RECORD +0 -58
- scikit_base-0.4.6.dist-info/top_level.txt +0 -2
skbase/utils/deep_equals.py
CHANGED
@@ -1,358 +1,358 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""Testing utility to compare equality in value for nested objects.
|
3
|
-
|
4
|
-
Objects compared can have one of the following valid types:
|
5
|
-
types compatible with != comparison
|
6
|
-
pd.Series, pd.DataFrame, np.ndarray
|
7
|
-
lists, tuples, or dicts of a valid type (recursive)
|
8
|
-
"""
|
9
|
-
from inspect import isclass
|
10
|
-
from typing import List
|
11
|
-
|
12
|
-
__author__: List[str] = ["fkiraly"]
|
13
|
-
__all__: List[str] = ["deep_equals"]
|
14
|
-
|
15
|
-
|
16
|
-
# flag variables for available soft dependencies
|
17
|
-
# we are not using _check_soft_dependencies in order to keep
|
18
|
-
# this utility uncoupled from the dependency on "packaging", of _check_soft_dependencies
|
19
|
-
def _softdep_available(importname):
|
20
|
-
from importlib import import_module
|
21
|
-
|
22
|
-
try:
|
23
|
-
import_module(importname)
|
24
|
-
except ModuleNotFoundError:
|
25
|
-
return False
|
26
|
-
else:
|
27
|
-
return True
|
28
|
-
|
29
|
-
|
30
|
-
numpy_available = _softdep_available("numpy")
|
31
|
-
pandas_available = _softdep_available("pandas")
|
32
|
-
|
33
|
-
|
34
|
-
def deep_equals(x, y, return_msg=False):
|
35
|
-
"""Test two objects for equality in value.
|
36
|
-
|
37
|
-
Correct if x/y are one of the following valid types:
|
38
|
-
types compatible with != comparison
|
39
|
-
pd.Series, pd.DataFrame, np.ndarray
|
40
|
-
lists, tuples, or dicts of a valid type (recursive)
|
41
|
-
|
42
|
-
Important note:
|
43
|
-
this function will return "not equal" if types of x,y are different
|
44
|
-
for instant, bool and numpy.bool are *not* considered equal
|
45
|
-
|
46
|
-
Parameters
|
47
|
-
----------
|
48
|
-
x : object
|
49
|
-
y : object
|
50
|
-
return_msg : bool, optional, default=False
|
51
|
-
whether to return informative message about what is not equal
|
52
|
-
|
53
|
-
Returns
|
54
|
-
-------
|
55
|
-
is_equal: bool - True if x and y are equal in value
|
56
|
-
x and y do not need to be equal in reference
|
57
|
-
msg : str, only returned if return_msg = True
|
58
|
-
indication of what is the reason for not being equal
|
59
|
-
concatenation of the following strings:
|
60
|
-
.type - type is not equal
|
61
|
-
.class - both objects are classes but not equal
|
62
|
-
.len - length is not equal
|
63
|
-
.value - value is not equal
|
64
|
-
.keys - if dict, keys of dict are not equal
|
65
|
-
if class/object, names of attributes and methods are not equal
|
66
|
-
.dtype - dtype of pandas or numpy object is not equal
|
67
|
-
.index - index of pandas object is not equal
|
68
|
-
.series_equals, .df_equals, .index_equals - .equals of pd returns False
|
69
|
-
[i] - if tuple/list: i-th element not equal
|
70
|
-
[key] - if dict: value at key is not equal
|
71
|
-
[colname] - if pandas.DataFrame: column with name colname is not equal
|
72
|
-
!= - call to generic != returns False
|
73
|
-
"""
|
74
|
-
|
75
|
-
def ret(is_equal, msg):
|
76
|
-
if return_msg:
|
77
|
-
if is_equal:
|
78
|
-
msg = ""
|
79
|
-
return is_equal, msg
|
80
|
-
else:
|
81
|
-
return is_equal
|
82
|
-
|
83
|
-
if type(x)
|
84
|
-
return ret(False, f".type, x.type = {type(x)} != y.type = {type(y)}")
|
85
|
-
|
86
|
-
# we now know all types are the same
|
87
|
-
# so now we compare values
|
88
|
-
|
89
|
-
if numpy_available:
|
90
|
-
import numpy as np
|
91
|
-
|
92
|
-
# pandas is a soft dependency, so we compare pandas objects separately
|
93
|
-
# and only if pandas is installed in the environment
|
94
|
-
if _is_pandas(x) and pandas_available:
|
95
|
-
res = _pandas_equals(x, y, return_msg=return_msg)
|
96
|
-
if res is not None:
|
97
|
-
return _pandas_equals(x, y, return_msg=return_msg)
|
98
|
-
|
99
|
-
if numpy_available and _is_npndarray(x):
|
100
|
-
if x.dtype != y.dtype:
|
101
|
-
return ret(False, f".dtype, x.dtype = {x.dtype} != y.dtype = {y.dtype}")
|
102
|
-
return ret(np.array_equal(x, y, equal_nan=True), ".values")
|
103
|
-
# recursion through lists, tuples and dicts
|
104
|
-
elif isinstance(x, (list, tuple)):
|
105
|
-
return ret(*_tuple_equals(x, y, return_msg=True))
|
106
|
-
elif isinstance(x, dict):
|
107
|
-
return ret(*_dict_equals(x, y, return_msg=True))
|
108
|
-
elif _is_npnan(x):
|
109
|
-
return ret(_is_npnan(y), f"type(x)={type(x)} != type(y)={type(y)}")
|
110
|
-
elif isclass(x):
|
111
|
-
return ret(x == y, f".class, x={x.__name__} != y={y.__name__}")
|
112
|
-
elif type(x).__name__ == "ForecastingHorizon":
|
113
|
-
return ret(*_fh_equals(x, y, return_msg=True))
|
114
|
-
# this elif covers case where != is boolean
|
115
|
-
# some types return a vector upon !=, this is covered in the next elif
|
116
|
-
elif isinstance(x == y, bool):
|
117
|
-
return ret(x == y, f" !=, {x} != {y}")
|
118
|
-
# deal with the case where != returns a vector
|
119
|
-
elif numpy_available and np.any(x != y) or any(_coerce_list(x != y)):
|
120
|
-
return ret(False, f" !=, {x} != {y}")
|
121
|
-
|
122
|
-
return ret(True, "")
|
123
|
-
|
124
|
-
|
125
|
-
def _is_pandas(x):
|
126
|
-
clstr = type(x).__name__
|
127
|
-
if clstr in ["DataFrame", "Series"]:
|
128
|
-
return True
|
129
|
-
if clstr.endswith("Index"):
|
130
|
-
return True
|
131
|
-
else:
|
132
|
-
return False
|
133
|
-
|
134
|
-
|
135
|
-
def _is_npndarray(x):
|
136
|
-
clstr = type(x).__name__
|
137
|
-
return clstr == "ndarray"
|
138
|
-
|
139
|
-
|
140
|
-
def _is_npnan(x):
|
141
|
-
if numpy_available:
|
142
|
-
import numpy as np
|
143
|
-
|
144
|
-
return isinstance(x, float) and np.isnan(x)
|
145
|
-
|
146
|
-
else:
|
147
|
-
return False
|
148
|
-
|
149
|
-
|
150
|
-
def _coerce_list(x):
|
151
|
-
"""Coerce x to list."""
|
152
|
-
if not isinstance(x, (list, tuple)):
|
153
|
-
x = [x]
|
154
|
-
if isinstance(x, tuple):
|
155
|
-
x = list(x)
|
156
|
-
|
157
|
-
return x
|
158
|
-
|
159
|
-
|
160
|
-
def _pandas_equals(x, y, return_msg=False):
|
161
|
-
import pandas as pd
|
162
|
-
|
163
|
-
def ret(is_equal, msg):
|
164
|
-
if return_msg:
|
165
|
-
if is_equal:
|
166
|
-
msg = ""
|
167
|
-
return is_equal, msg
|
168
|
-
else:
|
169
|
-
return is_equal
|
170
|
-
|
171
|
-
if isinstance(x, pd.Series):
|
172
|
-
if x.dtype != y.dtype:
|
173
|
-
return ret(False, f".dtype, x.dtype= {x.dtype} != y.dtype = {y.dtype}")
|
174
|
-
# if columns are object, recurse over entries and index
|
175
|
-
if x.dtype == "object":
|
176
|
-
index_equal = x.index.equals(y.index)
|
177
|
-
values_equal, values_msg = deep_equals(
|
178
|
-
list(x.to_array()), list(y.to_array()), return_msg=True
|
179
|
-
)
|
180
|
-
if not values_equal:
|
181
|
-
msg = ".values" + values_msg
|
182
|
-
elif not index_equal:
|
183
|
-
msg = f".index, x.index: {x.index}, y.index: {y.index}"
|
184
|
-
else:
|
185
|
-
msg = ""
|
186
|
-
return ret(index_equal and values_equal, msg)
|
187
|
-
else:
|
188
|
-
return ret(x.equals(y), f".series_equals, x = {x} != y = {y}")
|
189
|
-
elif isinstance(x, pd.DataFrame):
|
190
|
-
if not x.columns.equals(y.columns):
|
191
|
-
return ret(
|
192
|
-
False, f".columns, x.columns = {x.columns} != y.columns = {y.columns}"
|
193
|
-
)
|
194
|
-
# if columns are equal and at least one is object, recurse over Series
|
195
|
-
if sum(x.dtypes == "object") > 0:
|
196
|
-
for c in x.columns:
|
197
|
-
is_equal, msg = deep_equals(x[c], y[c], return_msg=True)
|
198
|
-
if not is_equal:
|
199
|
-
return ret(False, f"[{c!r}]" + msg)
|
200
|
-
return ret(True, "")
|
201
|
-
else:
|
202
|
-
return ret(x.equals(y), f".df_equals, x = {x} != y = {y}")
|
203
|
-
elif isinstance(x, pd.Index):
|
204
|
-
return ret(x.equals(y), f".index_equals, x = {x} != y = {y}")
|
205
|
-
|
206
|
-
|
207
|
-
def _tuple_equals(x, y, return_msg=False):
|
208
|
-
"""Test two tuples or lists for equality.
|
209
|
-
|
210
|
-
Correct if tuples/lists contain the following valid types:
|
211
|
-
types compatible with != comparison
|
212
|
-
pd.Series, pd.DataFrame, np.ndarray
|
213
|
-
lists, tuples, or dicts of a valid type (recursive)
|
214
|
-
|
215
|
-
Parameters
|
216
|
-
----------
|
217
|
-
x: tuple or list
|
218
|
-
y: tuple or list
|
219
|
-
return_msg : bool, optional, default=False
|
220
|
-
whether to return informative message about what is not equal
|
221
|
-
|
222
|
-
Returns
|
223
|
-
-------
|
224
|
-
is_equal: bool - True if x and y are equal in value
|
225
|
-
x and y do not need to be equal in reference
|
226
|
-
msg : str, only returned if return_msg = True
|
227
|
-
indication of what is the reason for not being equal
|
228
|
-
concatenation of the following elements:
|
229
|
-
.len - length is not equal
|
230
|
-
[i] - i-th element not equal
|
231
|
-
"""
|
232
|
-
|
233
|
-
def ret(is_equal, msg):
|
234
|
-
if return_msg:
|
235
|
-
if is_equal:
|
236
|
-
msg = ""
|
237
|
-
return is_equal, msg
|
238
|
-
else:
|
239
|
-
return is_equal
|
240
|
-
|
241
|
-
n = len(x)
|
242
|
-
|
243
|
-
if n != len(y):
|
244
|
-
return ret(False, f".len, x.len = {n} != y.len = {len(y)}")
|
245
|
-
|
246
|
-
# we now know dicts are same length
|
247
|
-
for i in range(n):
|
248
|
-
xi = x[i]
|
249
|
-
yi = y[i]
|
250
|
-
|
251
|
-
# recurse through xi/yi
|
252
|
-
is_equal, msg = deep_equals(xi, yi, return_msg=True)
|
253
|
-
if not is_equal:
|
254
|
-
return ret(False, f"[{i}]" + msg)
|
255
|
-
|
256
|
-
return ret(True, "")
|
257
|
-
|
258
|
-
|
259
|
-
def _dict_equals(x, y, return_msg=False):
|
260
|
-
"""Test two dicts for equality.
|
261
|
-
|
262
|
-
Correct if dicts contain the following valid types:
|
263
|
-
types compatible with != comparison
|
264
|
-
pd.Series, pd.DataFrame, np.ndarray
|
265
|
-
lists, tuples, or dicts of a valid type (recursive)
|
266
|
-
|
267
|
-
Parameters
|
268
|
-
----------
|
269
|
-
x: dict
|
270
|
-
y: dict
|
271
|
-
return_msg : bool, optional, default=False
|
272
|
-
whether to return informative message about what is not equal
|
273
|
-
|
274
|
-
Returns
|
275
|
-
-------
|
276
|
-
is_equal: bool - True if x and y are equal in value
|
277
|
-
x and y do not need to be equal in reference
|
278
|
-
msg : str, only returned if return_msg = True
|
279
|
-
indication of what is the reason for not being equal
|
280
|
-
concatenation of the following strings:
|
281
|
-
.keys - keys are not equal
|
282
|
-
[key] - values at key is not equal
|
283
|
-
"""
|
284
|
-
|
285
|
-
def ret(is_equal, msg):
|
286
|
-
if return_msg:
|
287
|
-
if is_equal:
|
288
|
-
msg = ""
|
289
|
-
return is_equal, msg
|
290
|
-
else:
|
291
|
-
return is_equal
|
292
|
-
|
293
|
-
xkeys = set(x.keys())
|
294
|
-
ykeys = set(y.keys())
|
295
|
-
|
296
|
-
if xkeys != ykeys:
|
297
|
-
xmy = xkeys.difference(ykeys)
|
298
|
-
ymx = ykeys.difference(xkeys)
|
299
|
-
diffmsg = ".keys,"
|
300
|
-
if len(xmy) > 0:
|
301
|
-
diffmsg += f" x.keys-y.keys = {xmy}."
|
302
|
-
if len(ymx) > 0:
|
303
|
-
diffmsg += f" y.keys-x.keys = {ymx}."
|
304
|
-
return ret(False, diffmsg)
|
305
|
-
|
306
|
-
# we now know that xkeys == ykeys
|
307
|
-
for key in xkeys:
|
308
|
-
xi = x[key]
|
309
|
-
yi = y[key]
|
310
|
-
|
311
|
-
# recurse through xi/yi
|
312
|
-
is_equal, msg = deep_equals(xi, yi, return_msg=True)
|
313
|
-
if not is_equal:
|
314
|
-
return ret(False, f"[{key}]" + msg)
|
315
|
-
|
316
|
-
return ret(True, "")
|
317
|
-
|
318
|
-
|
319
|
-
def _fh_equals(x, y, return_msg=False):
|
320
|
-
"""Test two forecasting horizons for equality.
|
321
|
-
|
322
|
-
Correct if both x and y are ForecastingHorizon
|
323
|
-
|
324
|
-
Parameters
|
325
|
-
----------
|
326
|
-
x: ForcastingHorizon
|
327
|
-
y: ForcastingHorizon
|
328
|
-
return_msg : bool, optional, default=False
|
329
|
-
whether to return informative message about what is not equal
|
330
|
-
|
331
|
-
Returns
|
332
|
-
-------
|
333
|
-
is_equal: bool - True if x and y are equal in value
|
334
|
-
x and y do not need to be equal in reference
|
335
|
-
msg : str, only returned if return_msg = True
|
336
|
-
indication of what is the reason for not being equal
|
337
|
-
concatenation of the following strings:
|
338
|
-
.is_relative - x is absolute and y is relative, or vice versa
|
339
|
-
.values - values of x and y are not equal
|
340
|
-
"""
|
341
|
-
|
342
|
-
def ret(is_equal, msg):
|
343
|
-
if return_msg:
|
344
|
-
if is_equal:
|
345
|
-
msg = ""
|
346
|
-
return is_equal, msg
|
347
|
-
else:
|
348
|
-
return is_equal
|
349
|
-
|
350
|
-
if x.is_relative != y.is_relative:
|
351
|
-
return ret(False, ".is_relative")
|
352
|
-
|
353
|
-
# recurse through values of x, y
|
354
|
-
is_equal, msg = deep_equals(x._values, y._values, return_msg=True)
|
355
|
-
if not is_equal:
|
356
|
-
return ret(False, ".values" + msg)
|
357
|
-
|
358
|
-
return ret(True, "")
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""Testing utility to compare equality in value for nested objects.
|
3
|
+
|
4
|
+
Objects compared can have one of the following valid types:
|
5
|
+
types compatible with != comparison
|
6
|
+
pd.Series, pd.DataFrame, np.ndarray
|
7
|
+
lists, tuples, or dicts of a valid type (recursive)
|
8
|
+
"""
|
9
|
+
from inspect import isclass
|
10
|
+
from typing import List
|
11
|
+
|
12
|
+
__author__: List[str] = ["fkiraly"]
|
13
|
+
__all__: List[str] = ["deep_equals"]
|
14
|
+
|
15
|
+
|
16
|
+
# flag variables for available soft dependencies
|
17
|
+
# we are not using _check_soft_dependencies in order to keep
|
18
|
+
# this utility uncoupled from the dependency on "packaging", of _check_soft_dependencies
|
19
|
+
def _softdep_available(importname):
|
20
|
+
from importlib import import_module
|
21
|
+
|
22
|
+
try:
|
23
|
+
import_module(importname)
|
24
|
+
except ModuleNotFoundError:
|
25
|
+
return False
|
26
|
+
else:
|
27
|
+
return True
|
28
|
+
|
29
|
+
|
30
|
+
numpy_available = _softdep_available("numpy")
|
31
|
+
pandas_available = _softdep_available("pandas")
|
32
|
+
|
33
|
+
|
34
|
+
def deep_equals(x, y, return_msg=False):
|
35
|
+
"""Test two objects for equality in value.
|
36
|
+
|
37
|
+
Correct if x/y are one of the following valid types:
|
38
|
+
types compatible with != comparison
|
39
|
+
pd.Series, pd.DataFrame, np.ndarray
|
40
|
+
lists, tuples, or dicts of a valid type (recursive)
|
41
|
+
|
42
|
+
Important note:
|
43
|
+
this function will return "not equal" if types of x,y are different
|
44
|
+
for instant, bool and numpy.bool are *not* considered equal
|
45
|
+
|
46
|
+
Parameters
|
47
|
+
----------
|
48
|
+
x : object
|
49
|
+
y : object
|
50
|
+
return_msg : bool, optional, default=False
|
51
|
+
whether to return informative message about what is not equal
|
52
|
+
|
53
|
+
Returns
|
54
|
+
-------
|
55
|
+
is_equal: bool - True if x and y are equal in value
|
56
|
+
x and y do not need to be equal in reference
|
57
|
+
msg : str, only returned if return_msg = True
|
58
|
+
indication of what is the reason for not being equal
|
59
|
+
concatenation of the following strings:
|
60
|
+
.type - type is not equal
|
61
|
+
.class - both objects are classes but not equal
|
62
|
+
.len - length is not equal
|
63
|
+
.value - value is not equal
|
64
|
+
.keys - if dict, keys of dict are not equal
|
65
|
+
if class/object, names of attributes and methods are not equal
|
66
|
+
.dtype - dtype of pandas or numpy object is not equal
|
67
|
+
.index - index of pandas object is not equal
|
68
|
+
.series_equals, .df_equals, .index_equals - .equals of pd returns False
|
69
|
+
[i] - if tuple/list: i-th element not equal
|
70
|
+
[key] - if dict: value at key is not equal
|
71
|
+
[colname] - if pandas.DataFrame: column with name colname is not equal
|
72
|
+
!= - call to generic != returns False
|
73
|
+
"""
|
74
|
+
|
75
|
+
def ret(is_equal, msg):
|
76
|
+
if return_msg:
|
77
|
+
if is_equal:
|
78
|
+
msg = ""
|
79
|
+
return is_equal, msg
|
80
|
+
else:
|
81
|
+
return is_equal
|
82
|
+
|
83
|
+
if type(x) is not type(y):
|
84
|
+
return ret(False, f".type, x.type = {type(x)} != y.type = {type(y)}")
|
85
|
+
|
86
|
+
# we now know all types are the same
|
87
|
+
# so now we compare values
|
88
|
+
|
89
|
+
if numpy_available:
|
90
|
+
import numpy as np
|
91
|
+
|
92
|
+
# pandas is a soft dependency, so we compare pandas objects separately
|
93
|
+
# and only if pandas is installed in the environment
|
94
|
+
if _is_pandas(x) and pandas_available:
|
95
|
+
res = _pandas_equals(x, y, return_msg=return_msg)
|
96
|
+
if res is not None:
|
97
|
+
return _pandas_equals(x, y, return_msg=return_msg)
|
98
|
+
|
99
|
+
if numpy_available and _is_npndarray(x):
|
100
|
+
if x.dtype != y.dtype:
|
101
|
+
return ret(False, f".dtype, x.dtype = {x.dtype} != y.dtype = {y.dtype}")
|
102
|
+
return ret(np.array_equal(x, y, equal_nan=True), ".values")
|
103
|
+
# recursion through lists, tuples and dicts
|
104
|
+
elif isinstance(x, (list, tuple)):
|
105
|
+
return ret(*_tuple_equals(x, y, return_msg=True))
|
106
|
+
elif isinstance(x, dict):
|
107
|
+
return ret(*_dict_equals(x, y, return_msg=True))
|
108
|
+
elif _is_npnan(x):
|
109
|
+
return ret(_is_npnan(y), f"type(x)={type(x)} != type(y)={type(y)}")
|
110
|
+
elif isclass(x):
|
111
|
+
return ret(x == y, f".class, x={x.__name__} != y={y.__name__}")
|
112
|
+
elif type(x).__name__ == "ForecastingHorizon":
|
113
|
+
return ret(*_fh_equals(x, y, return_msg=True))
|
114
|
+
# this elif covers case where != is boolean
|
115
|
+
# some types return a vector upon !=, this is covered in the next elif
|
116
|
+
elif isinstance(x == y, bool):
|
117
|
+
return ret(x == y, f" !=, {x} != {y}")
|
118
|
+
# deal with the case where != returns a vector
|
119
|
+
elif numpy_available and np.any(x != y) or any(_coerce_list(x != y)):
|
120
|
+
return ret(False, f" !=, {x} != {y}")
|
121
|
+
|
122
|
+
return ret(True, "")
|
123
|
+
|
124
|
+
|
125
|
+
def _is_pandas(x):
|
126
|
+
clstr = type(x).__name__
|
127
|
+
if clstr in ["DataFrame", "Series"]:
|
128
|
+
return True
|
129
|
+
if clstr.endswith("Index"):
|
130
|
+
return True
|
131
|
+
else:
|
132
|
+
return False
|
133
|
+
|
134
|
+
|
135
|
+
def _is_npndarray(x):
|
136
|
+
clstr = type(x).__name__
|
137
|
+
return clstr == "ndarray"
|
138
|
+
|
139
|
+
|
140
|
+
def _is_npnan(x):
|
141
|
+
if numpy_available:
|
142
|
+
import numpy as np
|
143
|
+
|
144
|
+
return isinstance(x, float) and np.isnan(x)
|
145
|
+
|
146
|
+
else:
|
147
|
+
return False
|
148
|
+
|
149
|
+
|
150
|
+
def _coerce_list(x):
|
151
|
+
"""Coerce x to list."""
|
152
|
+
if not isinstance(x, (list, tuple)):
|
153
|
+
x = [x]
|
154
|
+
if isinstance(x, tuple):
|
155
|
+
x = list(x)
|
156
|
+
|
157
|
+
return x
|
158
|
+
|
159
|
+
|
160
|
+
def _pandas_equals(x, y, return_msg=False):
|
161
|
+
import pandas as pd
|
162
|
+
|
163
|
+
def ret(is_equal, msg):
|
164
|
+
if return_msg:
|
165
|
+
if is_equal:
|
166
|
+
msg = ""
|
167
|
+
return is_equal, msg
|
168
|
+
else:
|
169
|
+
return is_equal
|
170
|
+
|
171
|
+
if isinstance(x, pd.Series):
|
172
|
+
if x.dtype != y.dtype:
|
173
|
+
return ret(False, f".dtype, x.dtype= {x.dtype} != y.dtype = {y.dtype}")
|
174
|
+
# if columns are object, recurse over entries and index
|
175
|
+
if x.dtype == "object":
|
176
|
+
index_equal = x.index.equals(y.index)
|
177
|
+
values_equal, values_msg = deep_equals(
|
178
|
+
list(x.to_array()), list(y.to_array()), return_msg=True
|
179
|
+
)
|
180
|
+
if not values_equal:
|
181
|
+
msg = ".values" + values_msg
|
182
|
+
elif not index_equal:
|
183
|
+
msg = f".index, x.index: {x.index}, y.index: {y.index}"
|
184
|
+
else:
|
185
|
+
msg = ""
|
186
|
+
return ret(index_equal and values_equal, msg)
|
187
|
+
else:
|
188
|
+
return ret(x.equals(y), f".series_equals, x = {x} != y = {y}")
|
189
|
+
elif isinstance(x, pd.DataFrame):
|
190
|
+
if not x.columns.equals(y.columns):
|
191
|
+
return ret(
|
192
|
+
False, f".columns, x.columns = {x.columns} != y.columns = {y.columns}"
|
193
|
+
)
|
194
|
+
# if columns are equal and at least one is object, recurse over Series
|
195
|
+
if sum(x.dtypes == "object") > 0:
|
196
|
+
for c in x.columns:
|
197
|
+
is_equal, msg = deep_equals(x[c], y[c], return_msg=True)
|
198
|
+
if not is_equal:
|
199
|
+
return ret(False, f"[{c!r}]" + msg)
|
200
|
+
return ret(True, "")
|
201
|
+
else:
|
202
|
+
return ret(x.equals(y), f".df_equals, x = {x} != y = {y}")
|
203
|
+
elif isinstance(x, pd.Index):
|
204
|
+
return ret(x.equals(y), f".index_equals, x = {x} != y = {y}")
|
205
|
+
|
206
|
+
|
207
|
+
def _tuple_equals(x, y, return_msg=False):
|
208
|
+
"""Test two tuples or lists for equality.
|
209
|
+
|
210
|
+
Correct if tuples/lists contain the following valid types:
|
211
|
+
types compatible with != comparison
|
212
|
+
pd.Series, pd.DataFrame, np.ndarray
|
213
|
+
lists, tuples, or dicts of a valid type (recursive)
|
214
|
+
|
215
|
+
Parameters
|
216
|
+
----------
|
217
|
+
x: tuple or list
|
218
|
+
y: tuple or list
|
219
|
+
return_msg : bool, optional, default=False
|
220
|
+
whether to return informative message about what is not equal
|
221
|
+
|
222
|
+
Returns
|
223
|
+
-------
|
224
|
+
is_equal: bool - True if x and y are equal in value
|
225
|
+
x and y do not need to be equal in reference
|
226
|
+
msg : str, only returned if return_msg = True
|
227
|
+
indication of what is the reason for not being equal
|
228
|
+
concatenation of the following elements:
|
229
|
+
.len - length is not equal
|
230
|
+
[i] - i-th element not equal
|
231
|
+
"""
|
232
|
+
|
233
|
+
def ret(is_equal, msg):
|
234
|
+
if return_msg:
|
235
|
+
if is_equal:
|
236
|
+
msg = ""
|
237
|
+
return is_equal, msg
|
238
|
+
else:
|
239
|
+
return is_equal
|
240
|
+
|
241
|
+
n = len(x)
|
242
|
+
|
243
|
+
if n != len(y):
|
244
|
+
return ret(False, f".len, x.len = {n} != y.len = {len(y)}")
|
245
|
+
|
246
|
+
# we now know dicts are same length
|
247
|
+
for i in range(n):
|
248
|
+
xi = x[i]
|
249
|
+
yi = y[i]
|
250
|
+
|
251
|
+
# recurse through xi/yi
|
252
|
+
is_equal, msg = deep_equals(xi, yi, return_msg=True)
|
253
|
+
if not is_equal:
|
254
|
+
return ret(False, f"[{i}]" + msg)
|
255
|
+
|
256
|
+
return ret(True, "")
|
257
|
+
|
258
|
+
|
259
|
+
def _dict_equals(x, y, return_msg=False):
|
260
|
+
"""Test two dicts for equality.
|
261
|
+
|
262
|
+
Correct if dicts contain the following valid types:
|
263
|
+
types compatible with != comparison
|
264
|
+
pd.Series, pd.DataFrame, np.ndarray
|
265
|
+
lists, tuples, or dicts of a valid type (recursive)
|
266
|
+
|
267
|
+
Parameters
|
268
|
+
----------
|
269
|
+
x: dict
|
270
|
+
y: dict
|
271
|
+
return_msg : bool, optional, default=False
|
272
|
+
whether to return informative message about what is not equal
|
273
|
+
|
274
|
+
Returns
|
275
|
+
-------
|
276
|
+
is_equal: bool - True if x and y are equal in value
|
277
|
+
x and y do not need to be equal in reference
|
278
|
+
msg : str, only returned if return_msg = True
|
279
|
+
indication of what is the reason for not being equal
|
280
|
+
concatenation of the following strings:
|
281
|
+
.keys - keys are not equal
|
282
|
+
[key] - values at key is not equal
|
283
|
+
"""
|
284
|
+
|
285
|
+
def ret(is_equal, msg):
|
286
|
+
if return_msg:
|
287
|
+
if is_equal:
|
288
|
+
msg = ""
|
289
|
+
return is_equal, msg
|
290
|
+
else:
|
291
|
+
return is_equal
|
292
|
+
|
293
|
+
xkeys = set(x.keys())
|
294
|
+
ykeys = set(y.keys())
|
295
|
+
|
296
|
+
if xkeys != ykeys:
|
297
|
+
xmy = xkeys.difference(ykeys)
|
298
|
+
ymx = ykeys.difference(xkeys)
|
299
|
+
diffmsg = ".keys,"
|
300
|
+
if len(xmy) > 0:
|
301
|
+
diffmsg += f" x.keys-y.keys = {xmy}."
|
302
|
+
if len(ymx) > 0:
|
303
|
+
diffmsg += f" y.keys-x.keys = {ymx}."
|
304
|
+
return ret(False, diffmsg)
|
305
|
+
|
306
|
+
# we now know that xkeys == ykeys
|
307
|
+
for key in xkeys:
|
308
|
+
xi = x[key]
|
309
|
+
yi = y[key]
|
310
|
+
|
311
|
+
# recurse through xi/yi
|
312
|
+
is_equal, msg = deep_equals(xi, yi, return_msg=True)
|
313
|
+
if not is_equal:
|
314
|
+
return ret(False, f"[{key}]" + msg)
|
315
|
+
|
316
|
+
return ret(True, "")
|
317
|
+
|
318
|
+
|
319
|
+
def _fh_equals(x, y, return_msg=False):
|
320
|
+
"""Test two forecasting horizons for equality.
|
321
|
+
|
322
|
+
Correct if both x and y are ForecastingHorizon
|
323
|
+
|
324
|
+
Parameters
|
325
|
+
----------
|
326
|
+
x: ForcastingHorizon
|
327
|
+
y: ForcastingHorizon
|
328
|
+
return_msg : bool, optional, default=False
|
329
|
+
whether to return informative message about what is not equal
|
330
|
+
|
331
|
+
Returns
|
332
|
+
-------
|
333
|
+
is_equal: bool - True if x and y are equal in value
|
334
|
+
x and y do not need to be equal in reference
|
335
|
+
msg : str, only returned if return_msg = True
|
336
|
+
indication of what is the reason for not being equal
|
337
|
+
concatenation of the following strings:
|
338
|
+
.is_relative - x is absolute and y is relative, or vice versa
|
339
|
+
.values - values of x and y are not equal
|
340
|
+
"""
|
341
|
+
|
342
|
+
def ret(is_equal, msg):
|
343
|
+
if return_msg:
|
344
|
+
if is_equal:
|
345
|
+
msg = ""
|
346
|
+
return is_equal, msg
|
347
|
+
else:
|
348
|
+
return is_equal
|
349
|
+
|
350
|
+
if x.is_relative != y.is_relative:
|
351
|
+
return ret(False, ".is_relative")
|
352
|
+
|
353
|
+
# recurse through values of x, y
|
354
|
+
is_equal, msg = deep_equals(x._values, y._values, return_msg=True)
|
355
|
+
if not is_equal:
|
356
|
+
return ret(False, ".values" + msg)
|
357
|
+
|
358
|
+
return ret(True, "")
|