checktestdata 2026.3.3__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.
File without changes
@@ -0,0 +1,3 @@
1
+ from checktestdata.pyctd import main
2
+
3
+ main()
checktestdata/lib.py ADDED
@@ -0,0 +1,471 @@
1
+ import argparse
2
+ import re
3
+ import sys
4
+ from abc import ABC
5
+ from collections import Counter
6
+ from enum import Enum
7
+ from fractions import Fraction
8
+ from pathlib import Path
9
+
10
+ if hasattr(sys, "set_int_max_str_digits"):
11
+ sys.set_int_max_str_digits(0)
12
+
13
+ class _ValueType(ABC):
14
+ __slots__ = ("value",)
15
+
16
+ def __init__(self, value):
17
+ self.value = value
18
+
19
+ def __repr__(self):
20
+ return f"{self.__class__.__name__}({repr(self.value)})"
21
+
22
+ def __str__(self):
23
+ return f"{self.__class__.__name__}({self.value})"
24
+
25
+ def __bool__(self):
26
+ raise TypeError(f"object of type '{self.__class__.__name__}' has no bool()")
27
+
28
+ def __invert__(self):
29
+ raise TypeError(f"bad operand type for unary !: '{self.__class__.__name__}'")
30
+
31
+ class Boolean(_ValueType):
32
+ __slots__ = ()
33
+
34
+ @staticmethod
35
+ def _check_combine_type(lhs, rhs):
36
+ if lhs.__class__ != rhs.__class__:
37
+ raise TypeError(f"cannot combine {lhs.__class__.__name__} and {rhs.__class__.__name__}")
38
+
39
+ def __init__(self, value):
40
+ assert isinstance(value, bool)
41
+ super().__init__(value)
42
+
43
+ def __bool__(self):
44
+ return self.value
45
+
46
+ def __invert__(self):
47
+ return Boolean(not self.value)
48
+
49
+ def __and__(self, other):
50
+ Boolean._check_combine_type(self, other)
51
+ return Boolean(self.value and other.value)
52
+
53
+ def __or__(self, other):
54
+ Boolean._check_combine_type(self, other)
55
+ return Boolean(self.value or other.value)
56
+
57
+ class _CompareableValue(_ValueType, ABC):
58
+ __slots__ = ()
59
+
60
+ @staticmethod
61
+ def _check_compare_type(lhs, rhs):
62
+ if lhs.__class__ != rhs.__class__:
63
+ raise TypeError(f"cannot compare {lhs.__class__.__name__} and {rhs.__class__.__name__}")
64
+
65
+ def __hash__(self):
66
+ return hash(self.value)
67
+
68
+ def __eq__(self, other):
69
+ _CompareableValue._check_compare_type(self, other)
70
+ return Boolean(self.value == other.value)
71
+
72
+ def __ne__(self, other):
73
+ _CompareableValue._check_compare_type(self, other)
74
+ return Boolean(self.value != other.value)
75
+
76
+ def __lt__(self, other):
77
+ _CompareableValue._check_compare_type(self, other)
78
+ return Boolean(self.value < other.value)
79
+
80
+ def __le__(self, other):
81
+ _CompareableValue._check_compare_type(self, other)
82
+ return Boolean(self.value <= other.value)
83
+
84
+ def __ge__(self, other):
85
+ _CompareableValue._check_compare_type(self, other)
86
+ return Boolean(self.value >= other.value)
87
+
88
+ def __gt__(self, other):
89
+ _CompareableValue._check_compare_type(self, other)
90
+ return Boolean(self.value > other.value)
91
+
92
+ class String(_CompareableValue):
93
+ __slots__ = ()
94
+
95
+ def __init__(self, value):
96
+ assert isinstance(value, str)
97
+ super().__init__(value)
98
+
99
+ class Number(_CompareableValue):
100
+ __slots__ = ()
101
+
102
+ @staticmethod
103
+ def _check_combine_type(lhs, rhs):
104
+ if lhs.__class__ != rhs.__class__:
105
+ raise TypeError(f"cannot combine {lhs.__class__.__name__} and {rhs.__class__.__name__}")
106
+
107
+ def __init__(self, value):
108
+ assert isinstance(value, (int, Fraction))
109
+ super().__init__(value)
110
+
111
+ def is_integer(self):
112
+ # we check the type, not the value!
113
+ return isinstance(self.value, int)
114
+
115
+ def __index__(self):
116
+ if not self.is_integer():
117
+ raise TypeError("expected integer but got float")
118
+ return self.value
119
+
120
+ def __int__(self):
121
+ if not self.is_integer():
122
+ raise TypeError("expected integer but got float")
123
+ return self.value
124
+
125
+ def __neg__(self):
126
+ return Number(-self.value)
127
+
128
+ def __add__(self, other):
129
+ Number._check_combine_type(self, other)
130
+ return Number(self.value + other.value)
131
+
132
+ def __sub__(self, other):
133
+ Number._check_combine_type(self, other)
134
+ return Number(self.value - other.value)
135
+
136
+ def __mul__(self, other):
137
+ Number._check_combine_type(self, other)
138
+ return Number(self.value * other.value)
139
+
140
+ def __mod__(self, other):
141
+ Number._check_combine_type(self, other)
142
+ if self.is_integer() and other.is_integer():
143
+ res = self.value % other.value
144
+ if res != 0 and (self.value < 0) != (other.value < 0):
145
+ res -= other.value
146
+ return Number(res)
147
+ else:
148
+ #seems to be an error in Checktestdata
149
+ raise TypeError(f"can only perform modulo on integers")
150
+ #return Number(self.value % other.value)
151
+
152
+ def __truediv__(self, other):
153
+ Number._check_combine_type(self, other)
154
+ if self.is_integer() and other.is_integer():
155
+ res = abs(self.value) // abs(other.value)
156
+ if (self.value < 0) != (other.value < 0):
157
+ res = -res
158
+ return Number(res)
159
+ else:
160
+ return Number(self.value / other.value)
161
+
162
+ def __pow__(self, other):
163
+ if not other.is_integer() or other.value < 0 or other.value.bit_length() > sys.maxsize.bit_length() + 1:
164
+ raise TypeError(f"exponent must be an unsigned long")
165
+ return Number(self.value ** other.value)
166
+
167
+ class VarType:
168
+ __slots__ = ("name", "data", "entries", "value_count")
169
+
170
+ def __init__(self, name):
171
+ self.name = name
172
+ self.data = None
173
+ self.entries = {}
174
+ self.value_count = Counter()
175
+
176
+ def reset(self):
177
+ self.data = None
178
+ self.entries = {}
179
+ self.value_count = Counter()
180
+
181
+ def __getitem__(self, key):
182
+ if key == None:
183
+ if self.entries:
184
+ raise RuntimeError(f"{self.name} is an array")
185
+ if self.data is None:
186
+ raise RuntimeError(f"{self.name} is not assigned")
187
+ return self.data
188
+ else:
189
+ if self.data is not None:
190
+ raise RuntimeError(f"{self.name} is not an array")
191
+ if key not in self.entries:
192
+ raise RuntimeError(f"missing key in {self.name}")
193
+ return self.entries[key]
194
+
195
+ def __setitem__(self, key, value):
196
+ # TODO: handle var_a[None] = var_b[None]?
197
+ assert isinstance(value, _ValueType), self.name
198
+ if key == None:
199
+ if self.entries:
200
+ raise RuntimeError(f"cannot replace array {self.name} with single value")
201
+ self.data = value
202
+ else:
203
+ if self.data is not None:
204
+ raise RuntimeError(f"{self.name} is not an array")
205
+ for key_part in key:
206
+ # Checktestdata seems to enforce integers here
207
+ if not isinstance(key_part, Number) or not key_part.is_integer():
208
+ raise TypeError(f"key for {self.name} must be integer(s)")
209
+ if key in self.entries:
210
+ self.value_count[self.entries[key]] -= 1
211
+ self.entries[key] = value
212
+ self.value_count[value] += 1
213
+
214
+ def assert_array(method, arg):
215
+ if not isinstance(arg, VarType):
216
+ raise TypeError(f"{method} cannot be invoked with {arg.__class__.__name__}")
217
+ if arg.data is not None:
218
+ raise TypeError(f"{method} must be invoked with an array, but {arg.name} is a value")
219
+
220
+ def assert_type(method, arg, t):
221
+ if not isinstance(arg, t):
222
+ raise TypeError(f"{method} cannot be invoked with {arg.__class__.__name__}")
223
+
224
+ def msg_text(text):
225
+ special = {
226
+ " ": "<SPACE>",
227
+ "\n": "<NEWLINE>",
228
+ "": "<EOF>",
229
+ }
230
+ return special.get(text, text)
231
+
232
+ INTEGER_REGEX = re.compile(r"0|-?[1-9][0-9]*")
233
+ FLOAT_PARTS = re.compile(r"-?([0-9]*)(?:\.([0-9]*))?(?:[eE](.*))?")
234
+ class FLOAT_REGEX(Enum):
235
+ ANY = re.compile(r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?(?:0|[1-9][0-9]*))?")
236
+ FIXED = re.compile(r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?")
237
+ SCIENTIFIC = re.compile(r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?(?:0|[1-9][0-9]*))")
238
+
239
+ def msg(self):
240
+ return "float" if self == FLOAT_REGEX.ANY else f"{self.name.lower()} float"
241
+
242
+ class _Reader:
243
+ def __init__(self, raw):
244
+ self.raw = raw
245
+ self.pos = 0
246
+ self.line = 1
247
+ self.column = 1
248
+ self.space_tokenizer = re.compile(r'[\s]|[^\s]*', re.DOTALL | re.MULTILINE)
249
+
250
+ def _advance(self, text):
251
+ self.pos += len(text)
252
+ newline = text.find("\n")
253
+ if newline >= 0:
254
+ self.line += text.count("\n")
255
+ self.column = len(text) - newline
256
+ else:
257
+ self.column += len(text)
258
+
259
+ def peek_char(self):
260
+ return self.raw[self.pos:self.pos+1]
261
+
262
+ def peek_until_space(self):
263
+ return self.space_tokenizer.match(self.raw, self.pos).group()
264
+
265
+ def pop_string(self, expected):
266
+ if not self.raw.startswith(expected, self.pos):
267
+ got = self.raw[self.pos:self.pos+len(expected)]
268
+ msg = f"{self.line}:{self.column} got: {msg_text(got)}, but expected {msg_text(expected)}"
269
+ raise RuntimeError(msg)
270
+ self._advance(expected)
271
+
272
+ def pop_regex(self, regex):
273
+ match = regex.match(self.raw, self.pos)
274
+ if not match:
275
+ got = self.peek_until_space()
276
+ msg = f"{self.line}:{self.column} got: {msg_text(got)}, but expected '{regex.pattern}'"
277
+ raise RuntimeError(msg)
278
+ text = match.group()
279
+ self._advance(text)
280
+ return text
281
+
282
+ def pop_pattern(self, pattern):
283
+ regex = re.compile(pattern, re.DOTALL | re.MULTILINE)
284
+ return self.pop_regex(regex)
285
+
286
+ def pop_token(self, regex):
287
+ match = regex.match(self.raw, self.pos)
288
+ if not match:
289
+ return None, self.line, self.column
290
+ else:
291
+ text = match.group()
292
+ line, column = self.line, self.column
293
+ self.pos += len(text)
294
+ if text == "\n":
295
+ self.line += 1
296
+ self.column = 1
297
+ else:
298
+ self.column += len(text)
299
+ return text, line, column
300
+
301
+ class Constraints:
302
+ __slots__ = ("file", "entries")
303
+
304
+ def __init__(self, file):
305
+ self.file = file
306
+ self.entries = {}
307
+
308
+ def log(self, name, value, min_value, max_value):
309
+ if self.file is None or name is None:
310
+ return
311
+ a, b, c, d, e, f = self.entries.get(name, (False, False, min_value, max_value, value, value))
312
+ a |= value == min_value
313
+ b |= value == max_value
314
+ c = min(c, min_value)
315
+ d = max(d, max_value)
316
+ e = min(e, value)
317
+ f = max(f, value)
318
+ self.entries[name] = (a, b, c, d, e, f)
319
+
320
+ def write(self):
321
+ if self.file is None:
322
+ return
323
+ lines = []
324
+ for name, entries in self.entries.items():
325
+ a, b, c, d, e, f = entries
326
+ lines.append(f"{name} {name} {int(a)} {int(b)} {c} {d} {e} {f}")
327
+ self.file.write_text("\n".join(lines))
328
+
329
+ reader = None
330
+ constraints = None
331
+
332
+ def init_lib():
333
+ global reader, constraints
334
+ parser = argparse.ArgumentParser()
335
+ parser.add_argument(
336
+ "--constraints_file",
337
+ dest="constraints_file",
338
+ metavar="constraints_file",
339
+ default=None,
340
+ type=Path,
341
+ required=False,
342
+ help="The file to write constraints to file to use.",
343
+ )
344
+ args, unknown = parser.parse_known_args()
345
+ constraints = Constraints(args.constraints_file)
346
+
347
+ raw = sys.stdin.read()
348
+ reader = _Reader(raw)
349
+
350
+ def finalize_lib():
351
+ constraints.write()
352
+
353
+ # Methods used by Checktestdata
354
+
355
+ def MATCH(arg):
356
+ assert_type("MATCH", arg, String)
357
+ char = reader.peek_char()
358
+ if not char:
359
+ return False
360
+ return Boolean(char in arg.value)
361
+
362
+ def ISEOF():
363
+ return Boolean(not reader.peek_char())
364
+
365
+ def UNIQUE(arg, *args):
366
+ assert_array("UNIQUE", arg)
367
+ for other in args:
368
+ assert_array("UNIQUE", other)
369
+ if arg.entries.keys() != other.entries.keys():
370
+ raise RuntimeError(f"{arg.name} and {other.name} must have the same keys for UNIQUE")
371
+ def make_entry(key):
372
+ return (arg[key], *(other[key] for other in args))
373
+ unique = {make_entry(key) for key in arg.entries.keys()}
374
+ return Boolean(len(unique) == len(arg.entries))
375
+
376
+ def INARRAY(value, array):
377
+ assert isinstance(value, _ValueType)
378
+ assert_array("INARRAY", array)
379
+ return Boolean(array.value_count[value] > 0)
380
+
381
+ def STRLEN(arg):
382
+ assert_type("STRLEN", arg, String)
383
+ return Number(len(arg.value))
384
+
385
+ def SPACE():
386
+ reader.pop_string(" ")
387
+
388
+ def NEWLINE():
389
+ reader.pop_string("\n")
390
+
391
+ def EOF():
392
+ got = reader.peek_char()
393
+ if got:
394
+ msg = f"{reader.line}:{reader.column} got: {msg_text(got)}, but expected {msg_text('')}"
395
+ raise RuntimeError(msg)
396
+
397
+ def INT(min, max, constraint = None):
398
+ assert_type("INT", min, Number)
399
+ assert_type("INT", max, Number)
400
+ text, line, column = reader.pop_token(INTEGER_REGEX)
401
+ if text is None:
402
+ got = reader.peek_until_space()
403
+ raise RuntimeError(f"{line}:{column} expected an integer but got {msg_text(got)}")
404
+ value = int(text)
405
+ if value < min.value or value > max.value:
406
+ raise RuntimeError(f"{line}:{column} integer {text} outside of range [{min.value}, {max.value}]")
407
+ constraints.log(constraint, value, min.value, max.value)
408
+ return Number(value)
409
+
410
+ def FLOAT(min, max, constraint = None, option = FLOAT_REGEX.ANY):
411
+ assert isinstance(option, FLOAT_REGEX)
412
+ assert_type("FLOAT", min, Number)
413
+ assert_type("FLOAT", max, Number)
414
+ text, line, column = reader.pop_token(option.value)
415
+ if text is None:
416
+ got = reader.peek_until_space()
417
+ raise RuntimeError(f"{line}:{column} expected a {option.msg()} but got {msg_text(got)}")
418
+ value = Fraction(text)
419
+ if value < min.value or value > max.value:
420
+ raise RuntimeError(f"{line}:{column} float {text} outside of range [{min.value}, {max.value}]")
421
+ if text.startswith("-") and value == 0:
422
+ raise RuntimeError(f"{line}:{column} float {text} should have no sign")
423
+ constraints.log(constraint, value, min.value, max.value)
424
+ return Number(value)
425
+
426
+ def FLOATP(min, max, mindecimals, maxdecimals, constraint = None, option = FLOAT_REGEX.ANY):
427
+ assert isinstance(option, FLOAT_REGEX)
428
+ assert_type("FLOATP", min, Number)
429
+ assert_type("FLOATP", max, Number)
430
+ assert_type("FLOATP", mindecimals, Number)
431
+ assert_type("FLOATP", maxdecimals, Number)
432
+ if not isinstance(mindecimals.value, int) and mindecimals.value >= 0:
433
+ raise RuntimeError(f"FLOATP(mindecimals) must be a non-negative integer")
434
+ if not isinstance(maxdecimals.value, int) and maxdecimals.value >= 0:
435
+ raise RuntimeError(f"FLOATP(maxdecimals) must be a non-negative integer")
436
+ text, line, column = reader.pop_token(option.value)
437
+ if text is None:
438
+ got = reader.peek_until_space()
439
+ raise RuntimeError(f"{line}:{column} expected a {option.msg()} but got {msg_text(got)}")
440
+ leading, decimals, exponent = FLOAT_PARTS.fullmatch(text).groups()
441
+ decimals = 0 if decimals is None else len(decimals)
442
+ has_exp = exponent is not None
443
+ if decimals < mindecimals.value or decimals > maxdecimals.value:
444
+ raise RuntimeError(f"{line}:{column} float decimals outside of range [{mindecimals.value}, {maxdecimals.value}]")
445
+ if has_exp and (len(leading) != 1 or leading == "0"):
446
+ raise RuntimeError(f"{line}:{column} scientific float should have exactly one non-zero before the decimal dot")
447
+ value = Fraction(text)
448
+ if value < min.value or value > max.value:
449
+ raise RuntimeError(f"{line}:{column} float {text} outside of range [{min.value}, {max.value}]")
450
+ if text.startswith("-") and value == 0:
451
+ raise RuntimeError(f"{line}:{column} float {text} should have no sign")
452
+ constraints.log(constraint, value, min.value, max.value)
453
+ return Number(value)
454
+
455
+ def STRING(arg):
456
+ assert_type("STRING", arg, String)
457
+ reader.pop_string(arg.value)
458
+
459
+ def REGEX(arg):
460
+ assert_type("REGEX", arg, String)
461
+ return String(reader.pop_pattern(arg.value))
462
+
463
+ def ASSERT(arg):
464
+ assert_type("ASSERT", arg, Boolean)
465
+ if not arg.value:
466
+ raise RuntimeError("ASSERT failed")
467
+
468
+ def UNSET(*args):
469
+ for arg in args:
470
+ assert_type("UNSET", arg, VarType)
471
+ arg.reset()