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.
Files changed (60) hide show
  1. docs/source/conf.py +299 -299
  2. {scikit_base-0.4.6.dist-info → scikit_base-0.5.1.dist-info}/LICENSE +29 -29
  3. {scikit_base-0.4.6.dist-info → scikit_base-0.5.1.dist-info}/METADATA +160 -159
  4. scikit_base-0.5.1.dist-info/RECORD +58 -0
  5. {scikit_base-0.4.6.dist-info → scikit_base-0.5.1.dist-info}/WHEEL +1 -1
  6. scikit_base-0.5.1.dist-info/top_level.txt +5 -0
  7. {scikit_base-0.4.6.dist-info → scikit_base-0.5.1.dist-info}/zip-safe +1 -1
  8. skbase/__init__.py +14 -14
  9. skbase/_exceptions.py +31 -31
  10. skbase/_nopytest_tests.py +35 -35
  11. skbase/base/__init__.py +20 -20
  12. skbase/base/_base.py +1249 -1249
  13. skbase/base/_meta.py +883 -871
  14. skbase/base/_pretty_printing/__init__.py +11 -11
  15. skbase/base/_pretty_printing/_object_html_repr.py +392 -392
  16. skbase/base/_pretty_printing/_pprint.py +412 -412
  17. skbase/base/_tagmanager.py +217 -217
  18. skbase/lookup/__init__.py +31 -31
  19. skbase/lookup/_lookup.py +1009 -1009
  20. skbase/lookup/tests/__init__.py +2 -2
  21. skbase/lookup/tests/test_lookup.py +991 -991
  22. skbase/testing/__init__.py +12 -12
  23. skbase/testing/test_all_objects.py +852 -856
  24. skbase/testing/utils/__init__.py +5 -5
  25. skbase/testing/utils/_conditional_fixtures.py +209 -209
  26. skbase/testing/utils/_dependencies.py +15 -15
  27. skbase/testing/utils/deep_equals.py +15 -15
  28. skbase/testing/utils/inspect.py +30 -30
  29. skbase/testing/utils/tests/__init__.py +2 -2
  30. skbase/testing/utils/tests/test_check_dependencies.py +49 -49
  31. skbase/testing/utils/tests/test_deep_equals.py +66 -66
  32. skbase/tests/__init__.py +2 -2
  33. skbase/tests/conftest.py +273 -273
  34. skbase/tests/mock_package/__init__.py +5 -5
  35. skbase/tests/mock_package/test_mock_package.py +74 -74
  36. skbase/tests/test_base.py +1202 -1202
  37. skbase/tests/test_baseestimator.py +130 -130
  38. skbase/tests/test_exceptions.py +23 -23
  39. skbase/tests/test_meta.py +170 -131
  40. skbase/utils/__init__.py +21 -21
  41. skbase/utils/_check.py +53 -53
  42. skbase/utils/_iter.py +238 -238
  43. skbase/utils/_nested_iter.py +180 -180
  44. skbase/utils/_utils.py +91 -91
  45. skbase/utils/deep_equals.py +358 -358
  46. skbase/utils/dependencies/__init__.py +11 -11
  47. skbase/utils/dependencies/_dependencies.py +253 -253
  48. skbase/utils/tests/__init__.py +4 -4
  49. skbase/utils/tests/test_check.py +24 -24
  50. skbase/utils/tests/test_iter.py +127 -127
  51. skbase/utils/tests/test_nested_iter.py +84 -84
  52. skbase/utils/tests/test_utils.py +37 -37
  53. skbase/validate/__init__.py +22 -22
  54. skbase/validate/_named_objects.py +403 -403
  55. skbase/validate/_types.py +345 -345
  56. skbase/validate/tests/__init__.py +2 -2
  57. skbase/validate/tests/test_iterable_named_objects.py +200 -200
  58. skbase/validate/tests/test_type_validations.py +370 -370
  59. scikit_base-0.4.6.dist-info/RECORD +0 -58
  60. scikit_base-0.4.6.dist-info/top_level.txt +0 -2
@@ -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) != 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, "")
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, "")