userver: en/testsuite/_internal/compare_transform.py Source File
Loading...
Searching...
No Matches
compare_transform.py
1import collections
2import contextlib
3import typing
4
5import py.io
6
7from testsuite.compare import CompareVisitor
8
9SetTypes = (set, frozenset)
10
11
13 path: list[str]
14 errors: typing.DefaultDict[str, list[str]]
15
16 _compare_visitors: list[CompareVisitor]
17
18 def __init__(
19 self,
20 compare_visitors: list[CompareVisitor] | None = None,
21 ):
22 self.path = ['left']
23 self.errors = collections.defaultdict(list)
24 self._compare_visitors = compare_visitors or []
25
26 def report_error(self, msg: str, *, path=None) -> None:
27 path_str = _build_path(self.path, path)
28 self.errors[path_str].append(msg)
29
30 def visit(
31 self, left: typing.Any, right: typing.Any
32 ) -> tuple[typing.Any, typing.Any]:
33 if left == right:
34 return left, left
35
36 left, right = _resolve_values(left, right, self.report_error)
37
38 for compare_visitor in reversed(self._compare_visitors):
39 if compare_visitor.predicate(left, right):
40 left, right = compare_visitor.visit(
41 left, right, self.report_error
42 )
43 return self.visit(left, right)
44
45 if isinstance(left, list):
46 return self.visit_list(left, right)
47 elif isinstance(left, dict):
48 return self.visit_dict(left, right)
49 elif isinstance(left, SetTypes):
50 return self.visit_set(left, right)
51
52 self.report_error(f'{py.io.saferepr(left)} != {py.io.saferepr(right)}')
53 return left, right
54
55 def visit_list(
56 self,
57 left: list | tuple,
58 right: typing.Any,
59 ) -> tuple:
60 if not isinstance(right, list):
61 self.report_error(
62 f'list expected on the right got {py.io.saferepr(right)} instead',
63 )
64 return left, right
65 left_len = len(left)
66 right_len = len(right)
67 if left_len != right_len:
68 self.report_error(
69 f'list length does not match: len(left)={left_len} len(right)={right_len}',
70 )
71
72 left_result = []
73 right_result = []
74 for idx, (item_left, item_right) in enumerate(
75 zip(left, right),
76 ):
77 with self.push(f'[{idx}]'):
78 left_mapped, right_mapped = self.visit(item_left, item_right)
79 left_result.append(left_mapped)
80 right_result.append(right_mapped)
81 if left_len > right_len:
82 for idx, item in enumerate(left[right_len:], right_len):
83 self.report_error(
84 f'[{idx}]: extra item on the left: {py.io.saferepr(item)}'
85 )
86 left_result.append(item)
87 elif right_len > left_len:
88 for idx, item in enumerate(right[left_len:], left_len):
89 self.report_error(
90 f'[{idx}]: extra item on the right: {py.io.saferepr(item)}'
91 )
92 right_result.append(item)
93 return left_result, right_result
94
95 def visit_dict(self, left: dict, right: typing.Any) -> tuple:
96 if not isinstance(right, dict):
97 self.report_error(
98 f'dict expected on the right, got {py.io.saferepr(right)} instead'
99 )
100 return left, right
101 left_len = len(left)
102 right_len = len(right)
103 if left_len != right_len:
104 self.report_error(
105 f'dict length does not match len(left)={left_len}, len(right)={right_len}'
106 )
107
108 common_keys = left.keys() & right.keys()
109 left_only = left.keys() - common_keys
110 right_only = right.keys() - common_keys
111
112 left_result = {}
113 right_result = {}
114
115 for key in common_keys | left_only:
116 left_result[key] = left[key]
117
118 if left_only:
119 self.report_error(
120 f'extra keys on the left: {_format_keys(left_only)}'
121 )
122 if right_only:
123 self.report_error(
124 f'extra keys on the right: {_format_keys(right_only)}'
125 )
126 for key in right_only:
127 right_result[key] = right[key]
128 for key in common_keys:
129 with self.push(f'[{key!r}]'):
130 left_mapped, right_mapped = self.visit(left[key], right[key])
131 left_result[key] = left_mapped
132 right_result[key] = right_mapped
133 return left_result, right_result
134
135 def visit_set(
136 self,
137 left: set | frozenset,
138 right: typing.Any,
139 ) -> tuple:
140 if not isinstance(right, SetTypes):
141 self.report_error(
142 f'set expected on the right got {py.io.saferepr(right)} instead',
143 )
144 return left, right
145 common_keys = left & right
146 left_only = left - common_keys
147 right_only = right - common_keys
148 right_result = set(common_keys)
149 if left_only:
150 self.report_error(
151 f'extra items on the left: {_format_keys(left_only)}',
152 )
153 if right_only:
154 self.report_error(
155 f'extra items on the right: {_format_keys(right_only)}',
156 )
157 for key in right_only:
158 right_result.add(key)
159 if isinstance(left, frozenset):
160 return left, frozenset(right_result)
161 return left, set(right_result)
162
163 @contextlib.contextmanager
164 def push(self, path: str):
165 try:
166 self.path.append(path)
167 yield
168 finally:
169 self.path.pop(-1)
170
171
172def _resolve_values(left, right, reporter):
173 if hasattr(left, '__testsuite_resolve_value__'):
174 return left.__testsuite_resolve_value__(right, reporter), right
175 if hasattr(right, '__testsuite_resolve_value__'):
176 return left, right.__testsuite_resolve_value__(left, reporter)
177 return left, right
178
179
180def _format_keys(keys):
181 return ', '.join(repr(key) for key in sorted(keys))
182
183
184def _build_path(path, extra_path=None):
185 realpath = path.copy()
186 if isinstance(extra_path, str):
187 realpath.append(extra_path)
188 elif isinstance(extra_path, (tuple, list)):
189 realpath.extend(extra_path)
190 return ''.join(realpath)