json-repair 0.44.1__py3-none-any.whl → 0.45.0__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.
- json_repair/json_context.py +2 -3
- json_repair/json_parser.py +58 -102
- json_repair/json_repair.py +16 -16
- json_repair/object_comparer.py +15 -31
- json_repair/string_file_wrapper.py +4 -7
- {json_repair-0.44.1.dist-info → json_repair-0.45.0.dist-info}/METADATA +1 -1
- json_repair-0.45.0.dist-info/RECORD +14 -0
- {json_repair-0.44.1.dist-info → json_repair-0.45.0.dist-info}/WHEEL +1 -1
- json_repair-0.44.1.dist-info/RECORD +0 -14
- {json_repair-0.44.1.dist-info → json_repair-0.45.0.dist-info}/entry_points.txt +0 -0
- {json_repair-0.44.1.dist-info → json_repair-0.45.0.dist-info}/licenses/LICENSE +0 -0
- {json_repair-0.44.1.dist-info → json_repair-0.45.0.dist-info}/top_level.txt +0 -0
json_repair/json_context.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
from enum import Enum, auto
|
2
|
-
from typing import List, Optional
|
3
2
|
|
4
3
|
|
5
4
|
class ContextValues(Enum):
|
@@ -10,8 +9,8 @@ class ContextValues(Enum):
|
|
10
9
|
|
11
10
|
class JsonContext:
|
12
11
|
def __init__(self) -> None:
|
13
|
-
self.context:
|
14
|
-
self.current:
|
12
|
+
self.context: list[ContextValues] = []
|
13
|
+
self.current: ContextValues | None = None
|
15
14
|
self.empty: bool = True
|
16
15
|
|
17
16
|
def set(self, value: ContextValues) -> None:
|
json_repair/json_parser.py
CHANGED
@@ -1,26 +1,27 @@
|
|
1
|
-
from typing import Any, ClassVar,
|
1
|
+
from typing import Any, ClassVar, Literal, TextIO
|
2
2
|
|
3
3
|
from .json_context import ContextValues, JsonContext
|
4
4
|
from .object_comparer import ObjectComparer
|
5
5
|
from .string_file_wrapper import StringFileWrapper
|
6
6
|
|
7
|
-
JSONReturnType =
|
7
|
+
JSONReturnType = dict[str, Any] | list[Any] | str | float | int | bool | None
|
8
8
|
|
9
9
|
|
10
10
|
class JSONParser:
|
11
11
|
# Constants
|
12
12
|
STRING_DELIMITERS: ClassVar[list[str]] = ['"', "'", "“", "”"]
|
13
|
+
NUMBER_CHARS: ClassVar[set[str]] = set("0123456789-.eE/,")
|
13
14
|
|
14
15
|
def __init__(
|
15
16
|
self,
|
16
|
-
json_str:
|
17
|
-
json_fd:
|
18
|
-
logging:
|
17
|
+
json_str: str | StringFileWrapper,
|
18
|
+
json_fd: TextIO | None,
|
19
|
+
logging: bool | None,
|
19
20
|
json_fd_chunk_length: int = 0,
|
20
21
|
stream_stable: bool = False,
|
21
22
|
) -> None:
|
22
23
|
# The string to parse
|
23
|
-
self.json_str:
|
24
|
+
self.json_str: str | StringFileWrapper = json_str
|
24
25
|
# Alternatively, the file description with a json file in it
|
25
26
|
if json_fd:
|
26
27
|
# This is a trick we do to treat the file wrapper as an array
|
@@ -31,12 +32,12 @@ class JSONParser:
|
|
31
32
|
self.context = JsonContext()
|
32
33
|
# Use this to log the activity, but only if logging is active
|
33
34
|
|
34
|
-
# This is a trick but a
|
35
|
+
# This is a trick but a beautiful one. We call self.log in the code over and over even if it's not needed.
|
35
36
|
# We could add a guard in the code for each call but that would make this code unreadable, so here's this neat trick
|
36
37
|
# Replace self.log with a noop
|
37
38
|
self.logging = logging
|
38
39
|
if logging:
|
39
|
-
self.logger:
|
40
|
+
self.logger: list[dict[str, str]] = []
|
40
41
|
self.log = self._log
|
41
42
|
else:
|
42
43
|
# No-op
|
@@ -52,14 +53,13 @@ class JSONParser:
|
|
52
53
|
|
53
54
|
def parse(
|
54
55
|
self,
|
55
|
-
) ->
|
56
|
+
) -> JSONReturnType | tuple[JSONReturnType, list[dict[str, str]]]:
|
56
57
|
json = self.parse_json()
|
57
58
|
if self.index < len(self.json_str):
|
58
59
|
self.log(
|
59
60
|
"The parser returned early, checking if there's more json elements",
|
60
61
|
)
|
61
62
|
json = [json]
|
62
|
-
last_index = self.index
|
63
63
|
while self.index < len(self.json_str):
|
64
64
|
j = self.parse_json()
|
65
65
|
if j != "":
|
@@ -67,9 +67,6 @@ class JSONParser:
|
|
67
67
|
# replace the last entry with the new one since the new one seems an update
|
68
68
|
json.pop()
|
69
69
|
json.append(j)
|
70
|
-
if self.index == last_index:
|
71
|
-
self.index += 1
|
72
|
-
last_index = self.index
|
73
70
|
# If nothing extra was found, don't return an array
|
74
71
|
if len(json) == 1:
|
75
72
|
self.log(
|
@@ -120,9 +117,9 @@ class JSONParser:
|
|
120
117
|
else:
|
121
118
|
self.index += 1
|
122
119
|
|
123
|
-
def parse_object(self) ->
|
120
|
+
def parse_object(self) -> dict[str, JSONReturnType]:
|
124
121
|
# <object> ::= '{' [ <member> *(', ' <member>) ] '}' ; A sequence of 'members'
|
125
|
-
obj:
|
122
|
+
obj: dict[str, JSONReturnType] = {}
|
126
123
|
# Stop when you either find the closing parentheses or you have iterated over the entire string
|
127
124
|
while (self.get_char_at() or "}") != "}":
|
128
125
|
# This is what we expect to find:
|
@@ -173,8 +170,6 @@ class JSONParser:
|
|
173
170
|
self.index += 1
|
174
171
|
self.skip_whitespaces_at()
|
175
172
|
continue
|
176
|
-
else:
|
177
|
-
self.index = rollback_index
|
178
173
|
key = str(self.parse_string())
|
179
174
|
if key == "":
|
180
175
|
self.skip_whitespaces_at()
|
@@ -228,7 +223,7 @@ class JSONParser:
|
|
228
223
|
self.index += 1
|
229
224
|
return obj
|
230
225
|
|
231
|
-
def parse_array(self) ->
|
226
|
+
def parse_array(self) -> list[JSONReturnType]:
|
232
227
|
# <array> ::= '[' [ <json> *(', ' <json>) ] ']' ; A sequence of JSON values separated by commas
|
233
228
|
arr = []
|
234
229
|
self.context.set(ContextValues.ARRAY)
|
@@ -265,7 +260,7 @@ class JSONParser:
|
|
265
260
|
self.context.reset()
|
266
261
|
return arr
|
267
262
|
|
268
|
-
def parse_string(self) ->
|
263
|
+
def parse_string(self) -> str | bool | None:
|
269
264
|
# <string> is a string of valid characters enclosed in quotes
|
270
265
|
# i.e. { name: "John" }
|
271
266
|
# Somehow all weird cases in an invalid JSON happen to be resolved in this function, so be careful here
|
@@ -312,59 +307,50 @@ class JSONParser:
|
|
312
307
|
self.index += 1
|
313
308
|
|
314
309
|
# There is sometimes a weird case of doubled quotes, we manage this also later in the while loop
|
315
|
-
if
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
):
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
310
|
+
if (
|
311
|
+
self.get_char_at() in self.STRING_DELIMITERS
|
312
|
+
and self.get_char_at() == lstring_delimiter
|
313
|
+
):
|
314
|
+
# If it's an empty key, this was easy
|
315
|
+
if (
|
316
|
+
self.context.current == ContextValues.OBJECT_KEY
|
317
|
+
and self.get_char_at(1) == ":"
|
318
|
+
):
|
319
|
+
self.index += 1
|
320
|
+
return ""
|
321
|
+
if self.get_char_at(1) == lstring_delimiter:
|
322
|
+
# There's something fishy about this, we found doubled quotes and then again quotes
|
323
|
+
self.log(
|
324
|
+
"While parsing a string, we found a doubled quote and then a quote again, ignoring it",
|
325
|
+
)
|
326
|
+
return ""
|
327
|
+
# Find the next delimiter
|
328
|
+
i = self.skip_to_character(character=rstring_delimiter, idx=1)
|
329
|
+
next_c = self.get_char_at(i)
|
330
|
+
# Now check that the next character is also a delimiter to ensure that we have "".....""
|
331
|
+
# In that case we ignore this rstring delimiter
|
332
|
+
if next_c and (self.get_char_at(i + 1) or "") == rstring_delimiter:
|
333
|
+
self.log(
|
334
|
+
"While parsing a string, we found a valid starting doubled quote",
|
335
|
+
)
|
336
|
+
doubled_quotes = True
|
337
|
+
self.index += 1
|
338
|
+
else:
|
339
|
+
# Ok this is not a doubled quote, check if this is an empty string or not
|
340
|
+
i = self.skip_whitespaces_at(idx=1, move_main_index=False)
|
333
341
|
next_c = self.get_char_at(i)
|
334
|
-
|
335
|
-
|
336
|
-
if next_c and (self.get_char_at(i + 1) or "") == rstring_delimiter:
|
342
|
+
if next_c in self.STRING_DELIMITERS + ["{", "["]:
|
343
|
+
# something fishy is going on here
|
337
344
|
self.log(
|
338
|
-
"While parsing a string, we found a
|
345
|
+
"While parsing a string, we found a doubled quote but also another quote afterwards, ignoring it",
|
339
346
|
)
|
340
|
-
doubled_quotes = True
|
341
347
|
self.index += 1
|
342
|
-
|
343
|
-
|
344
|
-
i = self.skip_whitespaces_at(idx=1, move_main_index=False)
|
345
|
-
next_c = self.get_char_at(i)
|
346
|
-
if next_c in [*self.STRING_DELIMITERS, "{", "["]:
|
347
|
-
# something fishy is going on here
|
348
|
-
self.log(
|
349
|
-
"While parsing a string, we found a doubled quote but also another quote afterwards, ignoring it",
|
350
|
-
)
|
351
|
-
self.index += 1
|
352
|
-
return ""
|
353
|
-
elif next_c not in [",", "]", "}"]:
|
354
|
-
self.log(
|
355
|
-
"While parsing a string, we found a doubled quote but it was a mistake, removing one quote",
|
356
|
-
)
|
357
|
-
self.index += 1
|
358
|
-
else:
|
359
|
-
# Otherwise we need to do another check before continuing
|
360
|
-
i = self.skip_to_character(character=rstring_delimiter, idx=1)
|
361
|
-
next_c = self.get_char_at(i)
|
362
|
-
if not next_c:
|
363
|
-
# mmmm that delimiter never appears again, this is a mistake
|
348
|
+
return ""
|
349
|
+
elif next_c not in [",", "]", "}"]:
|
364
350
|
self.log(
|
365
|
-
"While parsing a string, we found a quote but it was a mistake,
|
351
|
+
"While parsing a string, we found a doubled quote but it was a mistake, removing one quote",
|
366
352
|
)
|
367
|
-
|
353
|
+
self.index += 1
|
368
354
|
|
369
355
|
# Initialize our return value
|
370
356
|
string_acc = ""
|
@@ -413,10 +399,6 @@ class JSONParser:
|
|
413
399
|
# So we need to check if we find a new lstring_delimiter afterwards
|
414
400
|
# If we do, maybe this is a missing delimiter
|
415
401
|
i = self.skip_to_character(character=lstring_delimiter, idx=i)
|
416
|
-
if doubled_quotes:
|
417
|
-
i = self.skip_to_character(
|
418
|
-
character=lstring_delimiter, idx=i
|
419
|
-
)
|
420
402
|
next_c = self.get_char_at(i)
|
421
403
|
if not next_c:
|
422
404
|
rstring_delimiter_missing = False
|
@@ -457,8 +439,6 @@ class JSONParser:
|
|
457
439
|
# Ok then this is part of the string
|
458
440
|
rstring_delimiter_missing = False
|
459
441
|
break
|
460
|
-
elif c == "}":
|
461
|
-
break
|
462
442
|
if rstring_delimiter_missing:
|
463
443
|
self.log(
|
464
444
|
"While parsing a string missing the left delimiter in object value context, we found a , or } and we couldn't determine that a right delimiter was present. Stopping here",
|
@@ -610,15 +590,6 @@ class JSONParser:
|
|
610
590
|
i += 1
|
611
591
|
i = self.skip_whitespaces_at(idx=i, move_main_index=False)
|
612
592
|
next_c = self.get_char_at(i)
|
613
|
-
if next_c == "}":
|
614
|
-
# OK this is valid then
|
615
|
-
self.log(
|
616
|
-
"While parsing a string, we misplaced a quote that would have closed the string but has a different meaning here since this is the last element of the object, ignoring it",
|
617
|
-
)
|
618
|
-
unmatched_delimiter = not unmatched_delimiter
|
619
|
-
string_acc += str(char)
|
620
|
-
self.index += 1
|
621
|
-
char = self.get_char_at()
|
622
593
|
elif (
|
623
594
|
next_c == rstring_delimiter and self.get_char_at(i - 1) != "\\"
|
624
595
|
):
|
@@ -707,13 +678,12 @@ class JSONParser:
|
|
707
678
|
|
708
679
|
return string_acc
|
709
680
|
|
710
|
-
def parse_number(self) ->
|
681
|
+
def parse_number(self) -> float | int | str | JSONReturnType:
|
711
682
|
# <number> is a valid real number expressed in one of a number of given formats
|
712
683
|
number_str = ""
|
713
684
|
char = self.get_char_at()
|
714
685
|
is_array = self.context.current == ContextValues.ARRAY
|
715
|
-
NUMBER_CHARS
|
716
|
-
while char and char in NUMBER_CHARS and (not is_array or char != ","):
|
686
|
+
while char and char in self.NUMBER_CHARS and (not is_array or char != ","):
|
717
687
|
number_str += char
|
718
688
|
self.index += 1
|
719
689
|
char = self.get_char_at()
|
@@ -730,19 +700,16 @@ class JSONParser:
|
|
730
700
|
return str(number_str)
|
731
701
|
if "." in number_str or "e" in number_str or "E" in number_str:
|
732
702
|
return float(number_str)
|
733
|
-
elif number_str == "-":
|
734
|
-
# If there is a stray "-" this will throw an exception, throw away this character
|
735
|
-
return self.parse_json()
|
736
703
|
else:
|
737
704
|
return int(number_str)
|
738
705
|
except ValueError:
|
739
706
|
return number_str
|
740
707
|
|
741
|
-
def parse_boolean_or_null(self) ->
|
708
|
+
def parse_boolean_or_null(self) -> bool | str | None:
|
742
709
|
# <boolean> is one of the literal strings 'true', 'false', or 'null' (unquoted)
|
743
710
|
starting_index = self.index
|
744
711
|
char = (self.get_char_at() or "").lower()
|
745
|
-
value:
|
712
|
+
value: tuple[str, bool | None] | None = None
|
746
713
|
if char == "t":
|
747
714
|
value = ("true", True)
|
748
715
|
elif char == "f":
|
@@ -823,17 +790,9 @@ class JSONParser:
|
|
823
790
|
break
|
824
791
|
self.log(f"Found block comment: {comment}")
|
825
792
|
return ""
|
826
|
-
|
827
|
-
# Not a recognized comment pattern, skip the slash.
|
828
|
-
self.index += 1
|
829
|
-
return ""
|
830
|
-
|
831
|
-
else:
|
832
|
-
# Should not be reached: if for some reason the current character does not start a comment, skip it.
|
833
|
-
self.index += 1
|
834
|
-
return ""
|
793
|
+
return "" # pragma: no cover
|
835
794
|
|
836
|
-
def get_char_at(self, count: int = 0) ->
|
795
|
+
def get_char_at(self, count: int = 0) -> str | Literal[False]:
|
837
796
|
# Why not use something simpler? Because try/except in python is a faster alternative to an "if" statement that is often True
|
838
797
|
try:
|
839
798
|
return self.json_str[self.index + count]
|
@@ -873,9 +832,6 @@ class JSONParser:
|
|
873
832
|
char = self.json_str[self.index + idx]
|
874
833
|
except IndexError:
|
875
834
|
return idx
|
876
|
-
if self.index + idx > 0 and self.json_str[self.index + idx - 1] == "\\":
|
877
|
-
# Ah this is an escaped character, try again
|
878
|
-
return self.skip_to_character(character=character, idx=idx + 1)
|
879
835
|
return idx
|
880
836
|
|
881
837
|
def _log(self, text: str) -> None:
|
json_repair/json_repair.py
CHANGED
@@ -14,7 +14,7 @@ This module will parse the JSON file following the BNF definition:
|
|
14
14
|
<object> ::= '{' [ <member> *(', ' <member>) ] '}' ; A sequence of 'members'
|
15
15
|
<member> ::= <string> ': ' <json> ; A pair consisting of a name, and a JSON value
|
16
16
|
|
17
|
-
If something is wrong (a missing
|
17
|
+
If something is wrong (a missing parentheses or quotes for example) it will use a few simple heuristics to fix the JSON string:
|
18
18
|
- Add the missing parentheses if the parser believes that the array or object should be closed
|
19
19
|
- Quote strings or add missing single quotes
|
20
20
|
- Adjust whitespaces and remove line breaks
|
@@ -25,7 +25,7 @@ All supported use cases are in the unit tests
|
|
25
25
|
import argparse
|
26
26
|
import json
|
27
27
|
import sys
|
28
|
-
from typing import
|
28
|
+
from typing import Literal, TextIO, overload
|
29
29
|
|
30
30
|
from .json_parser import JSONParser, JSONReturnType
|
31
31
|
|
@@ -36,7 +36,7 @@ def repair_json(
|
|
36
36
|
return_objects: Literal[False] = False,
|
37
37
|
skip_json_loads: bool = False,
|
38
38
|
logging: bool = False,
|
39
|
-
json_fd:
|
39
|
+
json_fd: TextIO | None = None,
|
40
40
|
ensure_ascii: bool = True,
|
41
41
|
chunk_length: int = 0,
|
42
42
|
stream_stable: bool = False,
|
@@ -49,11 +49,11 @@ def repair_json(
|
|
49
49
|
return_objects: Literal[True] = True,
|
50
50
|
skip_json_loads: bool = False,
|
51
51
|
logging: bool = False,
|
52
|
-
json_fd:
|
52
|
+
json_fd: TextIO | None = None,
|
53
53
|
ensure_ascii: bool = True,
|
54
54
|
chunk_length: int = 0,
|
55
55
|
stream_stable: bool = False,
|
56
|
-
) ->
|
56
|
+
) -> JSONReturnType | tuple[JSONReturnType, list[dict[str, str]]]: ...
|
57
57
|
|
58
58
|
|
59
59
|
def repair_json(
|
@@ -61,11 +61,11 @@ def repair_json(
|
|
61
61
|
return_objects: bool = False,
|
62
62
|
skip_json_loads: bool = False,
|
63
63
|
logging: bool = False,
|
64
|
-
json_fd:
|
64
|
+
json_fd: TextIO | None = None,
|
65
65
|
ensure_ascii: bool = True,
|
66
66
|
chunk_length: int = 0,
|
67
67
|
stream_stable: bool = False,
|
68
|
-
) ->
|
68
|
+
) -> JSONReturnType | tuple[JSONReturnType, list[dict[str, str]]]:
|
69
69
|
"""
|
70
70
|
Given a json formatted string, it will try to decode it and, if it fails, it will try to fix it.
|
71
71
|
|
@@ -86,16 +86,16 @@ def repair_json(
|
|
86
86
|
parsed_json = parser.parse()
|
87
87
|
else:
|
88
88
|
try:
|
89
|
-
if json_fd
|
90
|
-
parsed_json = json.load(json_fd)
|
91
|
-
else:
|
92
|
-
parsed_json = json.loads(json_str)
|
89
|
+
parsed_json = json.load(json_fd) if json_fd else json.loads(json_str)
|
93
90
|
except json.JSONDecodeError:
|
94
91
|
parsed_json = parser.parse()
|
95
92
|
# It's useful to return the actual object instead of the json string,
|
96
93
|
# it allows this lib to be a replacement of the json library
|
97
94
|
if return_objects or logging:
|
98
95
|
return parsed_json
|
96
|
+
# Avoid returning only a pair of quotes if it's an empty string
|
97
|
+
elif parsed_json == "":
|
98
|
+
return ""
|
99
99
|
return json.dumps(parsed_json, ensure_ascii=ensure_ascii)
|
100
100
|
|
101
101
|
|
@@ -104,7 +104,7 @@ def loads(
|
|
104
104
|
skip_json_loads: bool = False,
|
105
105
|
logging: bool = False,
|
106
106
|
stream_stable: bool = False,
|
107
|
-
) ->
|
107
|
+
) -> JSONReturnType | tuple[JSONReturnType, list[dict[str, str]]] | str:
|
108
108
|
"""
|
109
109
|
This function works like `json.loads()` except that it will fix your JSON in the process.
|
110
110
|
It is a wrapper around the `repair_json()` function with `return_objects=True`.
|
@@ -131,7 +131,7 @@ def load(
|
|
131
131
|
skip_json_loads: bool = False,
|
132
132
|
logging: bool = False,
|
133
133
|
chunk_length: int = 0,
|
134
|
-
) ->
|
134
|
+
) -> JSONReturnType | tuple[JSONReturnType, list[dict[str, str]]]:
|
135
135
|
"""
|
136
136
|
This function works like `json.load()` except that it will fix your JSON in the process.
|
137
137
|
It is a wrapper around the `repair_json()` function with `json_fd=fd` and `return_objects=True`.
|
@@ -159,7 +159,7 @@ def from_file(
|
|
159
159
|
skip_json_loads: bool = False,
|
160
160
|
logging: bool = False,
|
161
161
|
chunk_length: int = 0,
|
162
|
-
) ->
|
162
|
+
) -> JSONReturnType | tuple[JSONReturnType, list[dict[str, str]]]:
|
163
163
|
"""
|
164
164
|
This function is a wrapper around `load()` so you can pass the filename as string
|
165
165
|
|
@@ -183,7 +183,7 @@ def from_file(
|
|
183
183
|
return jsonobj
|
184
184
|
|
185
185
|
|
186
|
-
def cli(inline_args:
|
186
|
+
def cli(inline_args: list[str] | None = None) -> int:
|
187
187
|
"""
|
188
188
|
Command-line interface for repairing and parsing JSON files.
|
189
189
|
|
@@ -267,7 +267,7 @@ def cli(inline_args: Optional[List[str]] = None) -> int:
|
|
267
267
|
else:
|
268
268
|
print(json.dumps(result, indent=args.indent, ensure_ascii=ensure_ascii))
|
269
269
|
except Exception as e: # pragma: no cover
|
270
|
-
print(f"Error: {e
|
270
|
+
print(f"Error: {str(e)}", file=sys.stderr)
|
271
271
|
return 1
|
272
272
|
|
273
273
|
return 0 # Success
|
json_repair/object_comparer.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
from typing import Any
|
2
2
|
|
3
3
|
|
4
|
-
class ObjectComparer:
|
4
|
+
class ObjectComparer: # pragma: no cover
|
5
5
|
def __init__(self) -> None:
|
6
|
-
|
6
|
+
pass # No operation performed in the constructor
|
7
7
|
|
8
8
|
@staticmethod
|
9
9
|
def is_same_object(obj1: Any, obj2: Any, path: str = "") -> bool:
|
@@ -16,40 +16,24 @@ class ObjectComparer:
|
|
16
16
|
# Fail immediately if the types don't match
|
17
17
|
return False
|
18
18
|
|
19
|
-
if isinstance(obj1, dict)
|
20
|
-
#
|
21
|
-
|
22
|
-
common_keys = keys1 & keys2
|
23
|
-
extra_keys1 = keys1 - keys2
|
24
|
-
extra_keys2 = keys2 - keys1
|
25
|
-
|
26
|
-
if extra_keys1:
|
27
|
-
return False
|
28
|
-
if extra_keys2:
|
19
|
+
if isinstance(obj1, dict):
|
20
|
+
# Quick length check before key compares
|
21
|
+
if len(obj1) != len(obj2):
|
29
22
|
return False
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
if not ObjectComparer.is_same_object(
|
34
|
-
obj1[key], obj2[key], path=f"{path}/{key}"
|
35
|
-
):
|
23
|
+
for key in obj1:
|
24
|
+
if key not in obj2:
|
25
|
+
return False
|
26
|
+
if not ObjectComparer.is_same_object(obj1[key], obj2[key]):
|
36
27
|
return False
|
28
|
+
return True
|
37
29
|
|
38
|
-
elif isinstance(obj1, list)
|
39
|
-
# Compare lists
|
40
|
-
min_length = min(len(obj1), len(obj2))
|
30
|
+
elif isinstance(obj1, list):
|
41
31
|
if len(obj1) != len(obj2):
|
42
32
|
return False
|
43
|
-
|
44
|
-
|
45
|
-
if not ObjectComparer.is_same_object(
|
46
|
-
obj1[i], obj2[i], path=f"{path}[{i}]"
|
47
|
-
):
|
33
|
+
for i in range(len(obj1)):
|
34
|
+
if not ObjectComparer.is_same_object(obj1[i], obj2[i]):
|
48
35
|
return False
|
36
|
+
return True
|
49
37
|
|
50
|
-
|
51
|
-
return False
|
52
|
-
elif len(obj2) > len(obj1):
|
53
|
-
return False
|
54
|
-
|
38
|
+
# For atoms: types already match, so just return True
|
55
39
|
return True
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import os
|
2
|
-
from typing import TextIO
|
2
|
+
from typing import TextIO
|
3
3
|
|
4
4
|
|
5
5
|
class StringFileWrapper:
|
@@ -48,7 +48,7 @@ class StringFileWrapper:
|
|
48
48
|
self.buffers.pop(oldest_key)
|
49
49
|
return self.buffers[index]
|
50
50
|
|
51
|
-
def __getitem__(self, index:
|
51
|
+
def __getitem__(self, index: int | slice) -> str:
|
52
52
|
"""
|
53
53
|
Retrieve a character or a slice of characters from the file.
|
54
54
|
|
@@ -97,7 +97,7 @@ class StringFileWrapper:
|
|
97
97
|
self.fd.seek(current_position)
|
98
98
|
return self.length
|
99
99
|
|
100
|
-
def __setitem__(self, index:
|
100
|
+
def __setitem__(self, index: int | slice, value: str) -> None: # pragma: no cover
|
101
101
|
"""
|
102
102
|
Set a character or a slice of characters in the file.
|
103
103
|
|
@@ -105,10 +105,7 @@ class StringFileWrapper:
|
|
105
105
|
index (slice): The slice of characters to set.
|
106
106
|
value (str): The value to set at the specified index or slice.
|
107
107
|
"""
|
108
|
-
if isinstance(index, slice)
|
109
|
-
start = index.start or 0
|
110
|
-
else:
|
111
|
-
start = index or 0
|
108
|
+
start = index.start or 0 if isinstance(index, slice) else index or 0
|
112
109
|
|
113
110
|
if start < 0:
|
114
111
|
start += len(self)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
json_repair/__init__.py,sha256=c4L2kZrHvWEKfj_ODU2naliNuvU6FlFVxtF0hbLe6s8,178
|
2
|
+
json_repair/__main__.py,sha256=EsJb-y89uZEvGQQg1GdIDWzfDwfOMvVekKEtdguQXCM,67
|
3
|
+
json_repair/json_context.py,sha256=WsMOjqpGSr6aaDONcrk8UFtTurzWon2Qq9AoBBYseoI,934
|
4
|
+
json_repair/json_parser.py,sha256=DnLeC0SKTs9yaixYBYZ3J4gwxGXwOLsTWHelptGUp4Q,39407
|
5
|
+
json_repair/json_repair.py,sha256=9wxf0vVNfr_RNQI1rbVPvxQ9feEwwvgnvkiYXwGEBX8,11292
|
6
|
+
json_repair/object_comparer.py,sha256=ZjxrzepSNGrhiwzid2Dm657x1Aj-E1-h37bDygK8ByE,1261
|
7
|
+
json_repair/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
json_repair/string_file_wrapper.py,sha256=uwW4B1s9Cf-iF3ANsCz-RPu2ddCqDETrt8bdojh8ufA,4485
|
9
|
+
json_repair-0.45.0.dist-info/licenses/LICENSE,sha256=wrjQo8MhNrNCicXtMe3MHmS-fx8AmQk1ue8AQwiiFV8,1076
|
10
|
+
json_repair-0.45.0.dist-info/METADATA,sha256=0RAn-AntRZtUdgJcBt8tvsZG-hHadaGfB9Vy2PBRcRk,12157
|
11
|
+
json_repair-0.45.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
12
|
+
json_repair-0.45.0.dist-info/entry_points.txt,sha256=SNfge3zPSP-ASqriYU9r3NAPaXdseYr7ciPMKdV2uSw,57
|
13
|
+
json_repair-0.45.0.dist-info/top_level.txt,sha256=7-VZwZN2CgB_n0NlSLk-rEUFh8ug21lESbsblOYuZqw,12
|
14
|
+
json_repair-0.45.0.dist-info/RECORD,,
|
@@ -1,14 +0,0 @@
|
|
1
|
-
json_repair/__init__.py,sha256=c4L2kZrHvWEKfj_ODU2naliNuvU6FlFVxtF0hbLe6s8,178
|
2
|
-
json_repair/__main__.py,sha256=EsJb-y89uZEvGQQg1GdIDWzfDwfOMvVekKEtdguQXCM,67
|
3
|
-
json_repair/json_context.py,sha256=mm6dOyrPJ1sDskTORZSXCW7W9-5veMlUKqXQ3Hw3EG4,971
|
4
|
-
json_repair/json_parser.py,sha256=wmDgXAroQ4gYZdi4Tbdn3LKXnx2x2v_uanzSzqP0aSQ,42003
|
5
|
-
json_repair/json_repair.py,sha256=r-Mtr16U_n2wmHX_zNRZI2ZlLc0AV0fLWlLzGEWjJa0,11312
|
6
|
-
json_repair/object_comparer.py,sha256=SeicB6_N4BHAEPon7s2BELEaJc4oyR9ZhfX2RgPk6Bw,1682
|
7
|
-
json_repair/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
json_repair/string_file_wrapper.py,sha256=koZmdq2-Z5K7XF1bDqX6dEbNaVMJYcMTjq-aGe6NQvA,4526
|
9
|
-
json_repair-0.44.1.dist-info/licenses/LICENSE,sha256=wrjQo8MhNrNCicXtMe3MHmS-fx8AmQk1ue8AQwiiFV8,1076
|
10
|
-
json_repair-0.44.1.dist-info/METADATA,sha256=VJv39wNseOemAA5tQe1dIW4ZPXFoLXFTPBCGLlLRwN8,12157
|
11
|
-
json_repair-0.44.1.dist-info/WHEEL,sha256=ooBFpIzZCPdw3uqIQsOo4qqbA4ZRPxHnOH7peeONza0,91
|
12
|
-
json_repair-0.44.1.dist-info/entry_points.txt,sha256=SNfge3zPSP-ASqriYU9r3NAPaXdseYr7ciPMKdV2uSw,57
|
13
|
-
json_repair-0.44.1.dist-info/top_level.txt,sha256=7-VZwZN2CgB_n0NlSLk-rEUFh8ug21lESbsblOYuZqw,12
|
14
|
-
json_repair-0.44.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|