Skip to content

Commit 9f0a02d

Browse files
committed
v0.0.8
1 parent 632c126 commit 9f0a02d

File tree

5 files changed

+119
-46
lines changed

5 files changed

+119
-46
lines changed

CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.0.8] - 2024-08-03
9+
10+
### Added
11+
12+
- Support `strict` mode based on [this issue](https://github.com/iw4p/partialjson/issues/5)
13+
- Test cases for `parser_strict` and `parser_non_strict` to handle incomplete and complete JSON strings with newline characters.
14+
- Example usage of both strict and non-strict parsers in the unit tests.
15+
- Unit tests for various number, string, boolean, array, and object parsing scenarios.
16+
17+
### Changed
18+
19+
- Updated incomplete number parsing logic to ensure better error handling and test coverage.
20+
21+
### Fixed
22+
23+
- Fixed issue with parsing incomplete floating point numbers where the parser incorrectly returned an error.
24+
- Corrected string parsing logic to properly handle escape characters in strict mode.
25+
26+
## [0.0.2] - 2023-11-24
27+
28+
### Added
29+
30+
### Changed
31+
32+
### Fixed
33+
34+
- json format
35+
36+
## [0.0.1] - 2023-11-24
37+
38+
### Added
39+
40+
- Initial implementation of `JSONParser` with support for only strict mode.

example.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
from partialjson.json_parser import JSONParser
22
import time, sys
33

4-
parser = JSONParser()
4+
parser_strict = JSONParser()
5+
parser_non_strict = JSONParser(strict=False)
6+
7+
print("###### Strict Mode == True (Default) ######")
8+
print(parser_strict.parse('{"x": "1st line\\n2nd line').get('x'))
9+
print(parser_strict.parse('{"x": "1st line\\n2nd line"').get('x'))
10+
print(parser_strict.parse('{"x": "1st line\\n2nd line"}').get('x'))
11+
print("###### Strict Mode == False ######")
12+
print(parser_non_strict.parse('{"x": "1st line\\n2nd line').get('x'))
13+
print(parser_non_strict.parse('{"x": "1st line\\n2nd line"').get('x'))
14+
print(parser_non_strict.parse('{"x": "1st line\\n2nd line"}').get('x'))
15+
516

617
incomplete_json = """
718
@@ -111,6 +122,6 @@
111122
for char in incomplete_json.strip():
112123
json += char
113124
print(f'\nIncomplete or streaming json:\n{json}')
114-
print(f'Final and usable JSON without crashing:\n{parser.parse(json)}')
125+
print(f'Final and usable JSON without crashing:\n{parser_strict.parse(json)}')
115126
sys.stdout.flush()
116127
time.sleep(0.01)

partialjson/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66
from .json_parser import JSONParser
77

8-
__version__ = "0.0.7"
8+
__version__ = "0.0.8"
99
__author__ = 'Nima Akbarzadeh'
1010
__author_email__ = "[email protected]"
1111
__license__ = "MIT"

partialjson/json_parser.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import json
22

33
class JSONParser:
4-
def __init__(self):
4+
def __init__(self, strict=True):
5+
self.strict = strict
56
self.parsers = {
67
' ': self.parse_space,
78
'\r': self.parse_space,
@@ -14,7 +15,6 @@ def __init__(self):
1415
'f': self.parse_false,
1516
'n': self.parse_null
1617
}
17-
# Adding parsers for numbers
1818
for c in '0123456789.-':
1919
self.parsers[c] = self.parse_number
2020

@@ -33,7 +33,7 @@ def parse(self, s):
3333
self.last_parse_reminding = reminding
3434
if self.on_extra_token and reminding:
3535
self.on_extra_token(s, data, reminding)
36-
return json.loads(json.dumps(data))
36+
return data
3737
else:
3838
return json.loads("{}")
3939

@@ -75,19 +75,16 @@ def parse_object(self, s, e):
7575
key, s = self.parse_any(s, e)
7676
s = s.strip()
7777

78-
# Handle case where object ends after a key
7978
if not s or s[0] == '}':
8079
acc[key] = None
8180
break
8281

83-
# Expecting a colon after the key
8482
if s[0] != ':':
8583
raise e # or handle this scenario as per your requirement
8684

8785
s = s[1:] # skip ':'
8886
s = s.strip()
8987

90-
# Handle case where value is missing or incomplete
9188
if not s or s[0] in ',}':
9289
acc[key] = None
9390
if s.startswith(','):
@@ -107,10 +104,15 @@ def parse_string(self, s, e):
107104
while end != -1 and s[end - 1] == '\\': # Handle escaped quotes
108105
end = s.find('"', end + 1)
109106
if end == -1:
110-
# Return the incomplete string without the opening quote
111-
return s[1:], ""
107+
# Incomplete string: handle it based on strict mode
108+
if not self.strict:
109+
return s[1:], ""
110+
else:
111+
return json.loads(f'"{s[1:]}"'), ""
112112
str_val = s[:end + 1]
113113
s = s[end + 1:]
114+
if not self.strict:
115+
return str_val[1:-1], s # Remove surrounding quotes for strict mode
114116
return json.loads(str_val), s
115117

116118
def parse_number(self, s, e):
@@ -143,5 +145,4 @@ def parse_false(self, s, e):
143145
def parse_null(self, s, e):
144146
if s.startswith('n'):
145147
return None, s[4:]
146-
raise e
147-
148+
raise e

test.py

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,82 +3,103 @@
33

44
class TestJSONParser(unittest.TestCase):
55
def setUp(self):
6-
self.parser = JSONParser()
6+
self.parser_strict = JSONParser(strict=True)
7+
self.parser_non_strict = JSONParser(strict=False)
78

9+
# Test for parser_strict
10+
def test_parser_strict_incomplete_object(self):
11+
with self.assertRaises(Exception):
12+
self.parser_strict.parse('{"x": "1st line\\n2nd line', '{"x": "1st line\\n2nd line"}')
13+
14+
def test_parser_strict_incomplete_string(self):
15+
with self.assertRaises(Exception):
16+
self.parser_strict.parse('{"x": "1st line\\n2nd line"', '{"x": "1st line\\n2nd line"}')
17+
18+
def test_parser_strict_complete_string(self):
19+
self.assertEqual(self.parser_strict.parse('{"x": "1st line\\n2nd line"}').get('x'), "1st line\n2nd line")
20+
21+
def test_parser_strict_incomplete_object(self):
22+
self.assertEqual(self.parser_strict.parse('{"x": "1st line\\n2nd line').get('x'), "1st line\n2nd line")
23+
24+
def test_parser_strict_incomplete_string(self):
25+
self.assertEqual(self.parser_strict.parse('{"x": "1st line\\n2nd line"').get('x'), "1st line\n2nd line")
26+
27+
# Test for parser_non_strict
28+
def test_parser_non_strict_complete_string(self):
29+
self.assertEqual(self.parser_non_strict.parse('{"x": "1st line\\n2nd line"}').get('x'), "1st line\n2nd line")
30+
31+
# Existing tests can remain unchanged...
832
# Number Tests
933
def test_positive_integer(self):
10-
self.assertEqual(self.parser.parse("42"), 42)
34+
self.assertEqual(self.parser_strict.parse("42"), 42)
1135

1236
def test_negative_integer(self):
13-
self.assertEqual(self.parser.parse("-42"), -42)
37+
self.assertEqual(self.parser_strict.parse("-42"), -42)
1438

1539
def test_positive_float(self):
16-
self.assertEqual(self.parser.parse("12.34"), 12.34)
40+
self.assertEqual(self.parser_strict.parse("12.34"), 12.34)
1741

1842
def test_negative_float(self):
19-
self.assertEqual(self.parser.parse("-12.34"), -12.34)
43+
self.assertEqual(self.parser_strict.parse("-12.34"), -12.34)
2044

2145
def test_incomplete_positive_float(self):
22-
self.assertEqual(self.parser.parse("12."), 12)
46+
self.assertEqual(self.parser_strict.parse("12."), 12)
2347

2448
def test_incomplete_negative_float(self):
25-
self.assertEqual(self.parser.parse("-12."), -12)
26-
27-
# def test_incomplete_negative_integer(self):
28-
# self.assertEqual(self.parser.parse("-"), -0)
49+
self.assertEqual(self.parser_strict.parse("-12."), -12)
2950

3051
def test_invalid_number(self):
3152
with self.assertRaises(Exception):
32-
self.parser.parse("1.2.3.4")
53+
self.parser_strict.parse("1.2.3.4")
3354

3455
# String Tests
3556
def test_string(self):
36-
self.assertEqual(self.parser.parse('"I am text"'), 'I am text')
37-
self.assertEqual(self.parser.parse('"I\'m text"'), "I'm text")
38-
self.assertEqual(self.parser.parse('"I\\"m text"'), 'I"m text')
57+
self.assertEqual(self.parser_strict.parse('"I am text"'), 'I am text')
58+
self.assertEqual(self.parser_strict.parse('"I\'m text"'), "I'm text")
59+
self.assertEqual(self.parser_strict.parse('"I\\"m text"'), 'I"m text')
3960

4061
def test_incomplete_string(self):
4162
with self.assertRaises(Exception):
42-
self.parser.parse('"I am text')
43-
self.parser.parse('"I\'m text')
44-
self.parser.parse('"I\\"m text')
63+
self.parser_strict.parse('"I am text', 'I am text')
64+
self.parser_strict.parse('"I\'m text', 'I\'m text')
65+
self.parser_strict.parse('"I\\"m text', 'I\\m text')
4566

4667
# Boolean Tests
4768
def test_boolean(self):
48-
self.assertEqual(self.parser.parse("true"), True)
49-
self.assertEqual(self.parser.parse("false"), False)
69+
self.assertEqual(self.parser_strict.parse("true"), True)
70+
self.assertEqual(self.parser_strict.parse("false"), False)
5071

5172
# Array Tests
5273
def test_empty_array(self):
53-
self.assertEqual(self.parser.parse("[]"), [])
74+
self.assertEqual(self.parser_strict.parse("[]"), [])
5475

5576
def test_number_array(self):
56-
self.assertEqual(self.parser.parse("[1,2,3]"), [1, 2, 3])
77+
self.assertEqual(self.parser_strict.parse("[1,2,3]"), [1, 2, 3])
5778

5879
def test_incomplete_array(self):
59-
self.assertEqual(self.parser.parse("[1,2,3"), [1, 2, 3])
60-
self.assertEqual(self.parser.parse("[1,2,"), [1, 2])
61-
self.assertEqual(self.parser.parse("[1,2"), [1, 2])
62-
self.assertEqual(self.parser.parse("[1,"), [1])
63-
self.assertEqual(self.parser.parse("[1"), [1])
64-
self.assertEqual(self.parser.parse("["), [])
80+
self.assertEqual(self.parser_strict.parse("[1,2,3"), [1, 2, 3])
81+
self.assertEqual(self.parser_strict.parse("[1,2,"), [1, 2])
82+
self.assertEqual(self.parser_strict.parse("[1,2"), [1, 2])
83+
self.assertEqual(self.parser_strict.parse("[1,"), [1])
84+
self.assertEqual(self.parser_strict.parse("[1"), [1])
85+
self.assertEqual(self.parser_strict.parse("["), [])
6586

6687
# Object Tests
6788
def test_simple_object(self):
6889
o = {"a": "apple", "b": "banana"}
69-
self.assertEqual(self.parser.parse('{"a":"apple","b":"banana"}'), o)
70-
self.assertEqual(self.parser.parse('{"a": "apple","b": "banana"}'), o)
71-
self.assertEqual(self.parser.parse('{"a" : "apple", "b" : "banana"}'), o)
90+
self.assertEqual(self.parser_strict.parse('{"a":"apple","b":"banana"}'), o)
91+
self.assertEqual(self.parser_strict.parse('{"a": "apple","b": "banana"}'), o)
92+
self.assertEqual(self.parser_strict.parse('{"a" : "apple", "b" : "banana"}'), o)
7293

7394
# Invalid Inputs
7495
def test_invalid_input(self):
7596
with self.assertRaises(Exception):
76-
self.parser.parse(":atom")
97+
self.parser_strict.parse(":atom")
7798

7899
# Extra Space
79100
def test_extra_space(self):
80-
self.assertEqual(self.parser.parse(" [1] "), [1])
81-
self.assertEqual(self.parser.parse(" [1 "), [1])
101+
self.assertEqual(self.parser_strict.parse(" [1] "), [1])
102+
self.assertEqual(self.parser_strict.parse(" [1 "), [1])
82103

83104
if __name__ == '__main__':
84105
unittest.main()

0 commit comments

Comments
 (0)