JsonhPy 2.2__tar.gz → 2.4__tar.gz
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.
- {jsonhpy-2.2 → jsonhpy-2.4}/PKG-INFO +1 -1
- {jsonhpy-2.2 → jsonhpy-2.4}/pyproject.toml +1 -1
- {jsonhpy-2.2 → jsonhpy-2.4}/src/JsonhPy/JsonhPy.py +207 -66
- {jsonhpy-2.2 → jsonhpy-2.4}/src/JsonhPy.egg-info/PKG-INFO +1 -1
- {jsonhpy-2.2 → jsonhpy-2.4}/LICENSE.md +0 -0
- {jsonhpy-2.2 → jsonhpy-2.4}/README.md +0 -0
- {jsonhpy-2.2 → jsonhpy-2.4}/setup.cfg +0 -0
- {jsonhpy-2.2 → jsonhpy-2.4}/src/JsonhPy/__init__.py +0 -0
- {jsonhpy-2.2 → jsonhpy-2.4}/src/JsonhPy.egg-info/SOURCES.txt +0 -0
- {jsonhpy-2.2 → jsonhpy-2.4}/src/JsonhPy.egg-info/dependency_links.txt +0 -0
- {jsonhpy-2.2 → jsonhpy-2.4}/src/JsonhPy.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import math
|
|
1
2
|
from enum import Enum
|
|
2
3
|
from typing import Iterator, Iterable
|
|
4
|
+
import json
|
|
3
5
|
|
|
4
6
|
class JsonhResult[T, E]:
|
|
5
7
|
is_error: bool
|
|
@@ -223,7 +225,15 @@ class JsonhNumberParser:
|
|
|
223
225
|
return exponent
|
|
224
226
|
|
|
225
227
|
# Multiply mantissa by 10 ^ exponent
|
|
226
|
-
|
|
228
|
+
try:
|
|
229
|
+
return JsonhResult.from_value(mantissa.value() * (10.0 ** exponent.value()))
|
|
230
|
+
except:
|
|
231
|
+
if mantissa.value() > 0:
|
|
232
|
+
return JsonhResult.from_value(math.inf)
|
|
233
|
+
elif mantissa.value() < 0:
|
|
234
|
+
return JsonhResult.from_value(-math.inf)
|
|
235
|
+
else:
|
|
236
|
+
return JsonhResult.from_value(0.0)
|
|
227
237
|
|
|
228
238
|
@staticmethod
|
|
229
239
|
def _parse_fractional_number(digits: str, base_digits: str) -> JsonhResult[float, str]:
|
|
@@ -238,29 +248,32 @@ class JsonhNumberParser:
|
|
|
238
248
|
|
|
239
249
|
# Get parts of number
|
|
240
250
|
whole_part: str = digits[:dot_index]
|
|
241
|
-
|
|
251
|
+
fraction_part: str = digits[(dot_index + 1):]
|
|
242
252
|
|
|
243
253
|
# Parse parts of number
|
|
244
|
-
whole: JsonhResult[
|
|
254
|
+
whole: JsonhResult[float, str] = JsonhNumberParser._parse_whole_number(whole_part, base_digits)
|
|
245
255
|
if whole.is_error:
|
|
246
256
|
return whole
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
257
|
+
|
|
258
|
+
# Add each column of fraction digits
|
|
259
|
+
fraction: float = 0.0
|
|
260
|
+
for index in range(len(fraction_part) - 1, -1, -1):
|
|
261
|
+
# Get current digit
|
|
262
|
+
digit_char: str = fraction_part[index]
|
|
263
|
+
digit_int: int = base_digits.find(digit_char.lower())
|
|
264
|
+
|
|
265
|
+
# Ensure digit is valid
|
|
266
|
+
if digit_int < 0:
|
|
267
|
+
return JsonhResult.from_error(f"Invalid digit: '{digit_char}'")
|
|
268
|
+
|
|
269
|
+
# Add value of column
|
|
270
|
+
fraction = (fraction + digit_int) / len(base_digits)
|
|
258
271
|
|
|
259
272
|
# Combine whole and fraction
|
|
260
|
-
return JsonhResult.from_value(
|
|
273
|
+
return JsonhResult.from_value(whole.value() + fraction)
|
|
261
274
|
|
|
262
275
|
@staticmethod
|
|
263
|
-
def _parse_whole_number(digits: str, base_digits: str) -> JsonhResult[
|
|
276
|
+
def _parse_whole_number(digits: str, base_digits: str) -> JsonhResult[float, str]:
|
|
264
277
|
"""
|
|
265
278
|
Converts a whole number (e.g. `12345`) from the given base (e.g. `01234567`) to a base-10 integer.
|
|
266
279
|
"""
|
|
@@ -274,7 +287,7 @@ class JsonhNumberParser:
|
|
|
274
287
|
digits = digits[1:]
|
|
275
288
|
|
|
276
289
|
# Add each column of digits
|
|
277
|
-
integer:
|
|
290
|
+
integer: float = 0.0
|
|
278
291
|
for index in range(0, len(digits)):
|
|
279
292
|
# Get current digit
|
|
280
293
|
digit_char: str = digits[index]
|
|
@@ -284,12 +297,8 @@ class JsonhNumberParser:
|
|
|
284
297
|
if digit_int < 0:
|
|
285
298
|
return JsonhResult.from_error(f"Invalid digit: '{digit_char}'")
|
|
286
299
|
|
|
287
|
-
# Get magnitude of current digit column
|
|
288
|
-
column_number: int = len(digits) - 1 - index
|
|
289
|
-
column_magnitude: int = len(base_digits) ** column_number
|
|
290
|
-
|
|
291
300
|
# Add value of column
|
|
292
|
-
integer
|
|
301
|
+
integer = (integer * len(base_digits)) + digit_int
|
|
293
302
|
|
|
294
303
|
# Apply sign
|
|
295
304
|
if sign != 1:
|
|
@@ -549,6 +558,132 @@ class JsonhReader:
|
|
|
549
558
|
|
|
550
559
|
return next_element
|
|
551
560
|
|
|
561
|
+
def parse_json(self, include_comments: bool = False, indent: str | None = None) -> JsonhResult[str, str]:
|
|
562
|
+
"""
|
|
563
|
+
Parses a single element as JSON from the reader.
|
|
564
|
+
|
|
565
|
+
If `include_comments` is true, comments are included (`/*` and `*/` are escaped as `/ *` and `* /`).
|
|
566
|
+
|
|
567
|
+
If `indent` is not null, the output is pretty-printed with the given indentation.
|
|
568
|
+
|
|
569
|
+
The result is not safe to embed in HTML.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
current_depth: int = 0
|
|
573
|
+
isStartOfStructure: bool = True
|
|
574
|
+
is_property_value: bool = False
|
|
575
|
+
|
|
576
|
+
resultBuilder: str = ""
|
|
577
|
+
|
|
578
|
+
for token_result in self.read_element():
|
|
579
|
+
# Check error
|
|
580
|
+
if (token_result.is_error):
|
|
581
|
+
return JsonhResult.from_error(token_result.error())
|
|
582
|
+
token: JsonhToken = token_result.value()
|
|
583
|
+
|
|
584
|
+
# Add comments and indents
|
|
585
|
+
if not is_property_value:
|
|
586
|
+
# Add comma before property/item
|
|
587
|
+
if (token.json_type not in [JsonTokenType.NONE, JsonTokenType.COMMENT]) and (current_depth > 0) and (not isStartOfStructure):
|
|
588
|
+
# Don't add trailing comma
|
|
589
|
+
if token.json_type not in [JsonTokenType.END_OBJECT, JsonTokenType.END_ARRAY]:
|
|
590
|
+
resultBuilder += ','
|
|
591
|
+
|
|
592
|
+
# Apply indentation
|
|
593
|
+
if indent != None:
|
|
594
|
+
# Don't indent inside empty structures
|
|
595
|
+
if not (token.json_type in [JsonTokenType.END_OBJECT, JsonTokenType.END_ARRAY] and isStartOfStructure):
|
|
596
|
+
# Don't indent comment if not included
|
|
597
|
+
if (not (token.json_type == JsonTokenType.COMMENT and (not include_comments))):
|
|
598
|
+
# Don't indent root elements
|
|
599
|
+
if current_depth > 0:
|
|
600
|
+
# Add newline before element
|
|
601
|
+
resultBuilder += '\n'
|
|
602
|
+
|
|
603
|
+
# Get current indent count
|
|
604
|
+
indent_count: int = current_depth
|
|
605
|
+
if token.json_type in [JsonTokenType.END_OBJECT, JsonTokenType.END_ARRAY]:
|
|
606
|
+
indent_count -= 1
|
|
607
|
+
|
|
608
|
+
# Add indent
|
|
609
|
+
for _ in range(0, indent_count):
|
|
610
|
+
resultBuilder += indent
|
|
611
|
+
# Track start of structure to avoid adding leading comma
|
|
612
|
+
if token.json_type not in [JsonTokenType.NONE, JsonTokenType.COMMENT]:
|
|
613
|
+
isStartOfStructure = False
|
|
614
|
+
if token.json_type in [JsonTokenType.START_OBJECT, JsonTokenType.START_ARRAY]:
|
|
615
|
+
isStartOfStructure = True
|
|
616
|
+
|
|
617
|
+
match token.json_type:
|
|
618
|
+
# Null
|
|
619
|
+
case JsonTokenType.NONE:
|
|
620
|
+
resultBuilder += "null"
|
|
621
|
+
if current_depth == 0:
|
|
622
|
+
return JsonhResult.from_value(resultBuilder)
|
|
623
|
+
# True
|
|
624
|
+
case JsonTokenType.TRUE:
|
|
625
|
+
resultBuilder += "true"
|
|
626
|
+
if current_depth == 0:
|
|
627
|
+
return JsonhResult.from_value(resultBuilder)
|
|
628
|
+
# False
|
|
629
|
+
case JsonTokenType.FALSE:
|
|
630
|
+
resultBuilder += "false"
|
|
631
|
+
if current_depth == 0:
|
|
632
|
+
return JsonhResult.from_value(resultBuilder)
|
|
633
|
+
# String
|
|
634
|
+
case JsonTokenType.STRING:
|
|
635
|
+
resultBuilder += json.dumps(token.value, ensure_ascii=False)
|
|
636
|
+
if current_depth == 0:
|
|
637
|
+
return JsonhResult.from_value(resultBuilder)
|
|
638
|
+
# Number
|
|
639
|
+
case JsonTokenType.NUMBER:
|
|
640
|
+
result: JsonhResult[float] = JsonhNumberParser.parse(token_result.value().value)
|
|
641
|
+
if (result.is_error):
|
|
642
|
+
return JsonhResult.from_error(result.error())
|
|
643
|
+
resultBuilder += str(result.value())
|
|
644
|
+
if current_depth == 0:
|
|
645
|
+
return JsonhResult.from_value(resultBuilder)
|
|
646
|
+
# Start Object
|
|
647
|
+
case JsonTokenType.START_OBJECT:
|
|
648
|
+
resultBuilder += '{'
|
|
649
|
+
current_depth += 1
|
|
650
|
+
# Start Array
|
|
651
|
+
case JsonTokenType.START_ARRAY:
|
|
652
|
+
resultBuilder += '['
|
|
653
|
+
current_depth += 1
|
|
654
|
+
# End Object
|
|
655
|
+
case JsonTokenType.END_OBJECT:
|
|
656
|
+
resultBuilder += '}'
|
|
657
|
+
current_depth -= 1
|
|
658
|
+
if current_depth == 0:
|
|
659
|
+
return JsonhResult.from_value(resultBuilder)
|
|
660
|
+
# End Array
|
|
661
|
+
case JsonTokenType.END_ARRAY:
|
|
662
|
+
resultBuilder += ']'
|
|
663
|
+
current_depth -= 1
|
|
664
|
+
if current_depth == 0:
|
|
665
|
+
return JsonhResult.from_value(resultBuilder)
|
|
666
|
+
# Property Name
|
|
667
|
+
case JsonTokenType.PROPERTY_NAME:
|
|
668
|
+
resultBuilder += json.dumps(token.value, ensure_ascii=False)
|
|
669
|
+
resultBuilder += ':'
|
|
670
|
+
if indent != None:
|
|
671
|
+
resultBuilder += ' '
|
|
672
|
+
# Comment
|
|
673
|
+
case JsonTokenType.COMMENT:
|
|
674
|
+
if (include_comments):
|
|
675
|
+
resultBuilder += "/*"
|
|
676
|
+
resultBuilder += token.value.replace("/*", "/ *").replace("*/", "* /")
|
|
677
|
+
resultBuilder += "*/"
|
|
678
|
+
# Not implemented
|
|
679
|
+
case _:
|
|
680
|
+
return JsonhResult.from_error("Token type not implemented")
|
|
681
|
+
|
|
682
|
+
is_property_value = token.json_type == JsonTokenType.PROPERTY_NAME
|
|
683
|
+
|
|
684
|
+
# End of input
|
|
685
|
+
return JsonhResult.from_error("Expected token, got end of input")
|
|
686
|
+
|
|
552
687
|
def find_property_value(self, property_name: str) -> bool:
|
|
553
688
|
"""
|
|
554
689
|
Tries to find the given property name in the reader.
|
|
@@ -1263,7 +1398,7 @@ class JsonhReader:
|
|
|
1263
1398
|
is_empty = False
|
|
1264
1399
|
# Dot
|
|
1265
1400
|
elif next == '.':
|
|
1266
|
-
# Disallow dot
|
|
1401
|
+
# Disallow dot following underscore
|
|
1267
1402
|
if len(number_builder.ref) >= 1 and number_builder.ref[-1] == '_':
|
|
1268
1403
|
return JsonhResult.from_error("`.` must not follow `_` in number")
|
|
1269
1404
|
|
|
@@ -1437,26 +1572,40 @@ class JsonhReader:
|
|
|
1437
1572
|
return
|
|
1438
1573
|
|
|
1439
1574
|
def _read_hex_sequence(self, length: int) -> JsonhResult[int, str]:
|
|
1440
|
-
|
|
1575
|
+
assert(length <= 8)
|
|
1441
1576
|
|
|
1442
|
-
|
|
1577
|
+
value: int = 0
|
|
1578
|
+
|
|
1579
|
+
for _ in range(0, length):
|
|
1443
1580
|
next: str | None = self._read()
|
|
1444
1581
|
|
|
1445
1582
|
# Hex digit
|
|
1446
1583
|
if next != None and ((ord('0') <= ord(next) <= ord('9')) or (ord('A') <= ord(next) <= ord('F')) or (ord('a') <= ord(next) <= ord('f'))):
|
|
1447
|
-
|
|
1584
|
+
# Get hex digit
|
|
1585
|
+
digit: int = ord(next)
|
|
1586
|
+
# Convert hex digit to integer
|
|
1587
|
+
integer: int = \
|
|
1588
|
+
digit - ord('A') + 10 if (digit >= ord('A') and digit <= ord('F')) else \
|
|
1589
|
+
digit - ord('a') + 10 if (digit >= ord('a') and digit <= ord('f')) else \
|
|
1590
|
+
digit - ord('0')
|
|
1591
|
+
# Aggregate digit into value
|
|
1592
|
+
value = (value * 16) + integer
|
|
1448
1593
|
# Unexpected char
|
|
1449
1594
|
else:
|
|
1450
1595
|
return JsonhResult.from_error("Incorrect number of hexadecimal digits in unicode escape sequence")
|
|
1451
1596
|
|
|
1452
|
-
#
|
|
1453
|
-
return JsonhResult.from_value(
|
|
1597
|
+
# Return aggregated value
|
|
1598
|
+
return JsonhResult.from_value(value)
|
|
1454
1599
|
|
|
1455
|
-
def _read_escape_sequence(self) -> JsonhResult[str, str]:
|
|
1600
|
+
def _read_escape_sequence(self, high_surrogate: int | None = None) -> JsonhResult[str, str]:
|
|
1456
1601
|
escape_char: str | None = self._read()
|
|
1457
1602
|
if escape_char == None:
|
|
1458
1603
|
return JsonhResult.from_error("Expected escape sequence, got end of input")
|
|
1459
1604
|
|
|
1605
|
+
# Ensure high surrogates are completed
|
|
1606
|
+
if high_surrogate != None and escape_char not in ['u', 'x', 'U']:
|
|
1607
|
+
return JsonhResult.from_error("Expected low surrogate after high surrogate")
|
|
1608
|
+
|
|
1460
1609
|
match escape_char:
|
|
1461
1610
|
# Reverse solidus
|
|
1462
1611
|
case '\\':
|
|
@@ -1490,13 +1639,13 @@ class JsonhReader:
|
|
|
1490
1639
|
return JsonhResult.from_value('\u001b')
|
|
1491
1640
|
# Unicode hex sequence
|
|
1492
1641
|
case 'u':
|
|
1493
|
-
return self._read_hex_escape_sequence(4)
|
|
1642
|
+
return self._read_hex_escape_sequence(4, high_surrogate)
|
|
1494
1643
|
# Short unicode hex sequence
|
|
1495
1644
|
case 'x':
|
|
1496
|
-
return self._read_hex_escape_sequence(2)
|
|
1645
|
+
return self._read_hex_escape_sequence(2, high_surrogate)
|
|
1497
1646
|
# Long unicode hex sequence
|
|
1498
1647
|
case 'U':
|
|
1499
|
-
return self._read_hex_escape_sequence(8)
|
|
1648
|
+
return self._read_hex_escape_sequence(8, high_surrogate)
|
|
1500
1649
|
# Escaped newline
|
|
1501
1650
|
case self._NEWLINE_CHARS:
|
|
1502
1651
|
# Join CR LF
|
|
@@ -1507,51 +1656,43 @@ class JsonhReader:
|
|
|
1507
1656
|
case _:
|
|
1508
1657
|
return JsonhResult.from_value(escape_char)
|
|
1509
1658
|
|
|
1510
|
-
def _read_hex_escape_sequence(self, length: int) -> JsonhResult[str, str]:
|
|
1511
|
-
# This method is used to combine escaped UTF-16 surrogate pairs (e.g. "\uD83D\uDC7D" -> "👽")
|
|
1512
|
-
|
|
1513
|
-
# Read hex digits & convert to uint
|
|
1659
|
+
def _read_hex_escape_sequence(self, length: int, high_surrogate: int | None) -> JsonhResult[str, str]:
|
|
1514
1660
|
code_point: JsonhResult[int, str] = self._read_hex_sequence(length)
|
|
1515
1661
|
if code_point.is_error:
|
|
1516
1662
|
return JsonhResult.from_error(code_point.error())
|
|
1517
1663
|
|
|
1518
|
-
#
|
|
1519
|
-
if
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
case 'x':
|
|
1532
|
-
low_code_point = self._read_hex_sequence(2)
|
|
1533
|
-
case 'U':
|
|
1534
|
-
low_code_point = self._read_hex_sequence(8)
|
|
1535
|
-
# Ensure hex sequence read successfully
|
|
1536
|
-
if low_code_point.is_error:
|
|
1537
|
-
return JsonhResult.from_error(low_code_point.error())
|
|
1538
|
-
# Combine high and low surrogates
|
|
1539
|
-
code_point.value_or_none = self._utf16_surrogates_to_code_point(code_point.value(), low_code_point.value())
|
|
1540
|
-
# Other escape sequence
|
|
1541
|
-
else:
|
|
1542
|
-
self.index = original_position
|
|
1543
|
-
|
|
1544
|
-
# Rune
|
|
1545
|
-
return JsonhResult.from_value(chr(code_point.value()))
|
|
1664
|
+
# Low surrogate
|
|
1665
|
+
if high_surrogate != None:
|
|
1666
|
+
combined: JsonhResult[int, str] = self._utf16_surrogates_to_code_point(high_surrogate, code_point.value())
|
|
1667
|
+
if combined.is_error:
|
|
1668
|
+
return JsonhResult.from_error(combined.error())
|
|
1669
|
+
return JsonhResult.from_value(chr(combined.value()))
|
|
1670
|
+
else:
|
|
1671
|
+
# High surrogate followed by low surrogate
|
|
1672
|
+
if self._is_utf16_high_surrogate(code_point.value()) and self._read_one('\\'):
|
|
1673
|
+
return self._read_escape_sequence(code_point.value())
|
|
1674
|
+
# Standalone character
|
|
1675
|
+
else:
|
|
1676
|
+
return JsonhResult.from_value(chr(code_point.value()))
|
|
1546
1677
|
|
|
1547
1678
|
@staticmethod
|
|
1548
|
-
def _utf16_surrogates_to_code_point(high_surrogate: int, low_surrogate: int) -> int:
|
|
1549
|
-
|
|
1679
|
+
def _utf16_surrogates_to_code_point(high_surrogate: int, low_surrogate: int) -> JsonhResult[int, str]:
|
|
1680
|
+
if not JsonhReader._is_utf16_high_surrogate(high_surrogate):
|
|
1681
|
+
return JsonhResult.from_error("High surrogate out of range")
|
|
1682
|
+
|
|
1683
|
+
if not JsonhReader._is_utf16_low_surrogate(low_surrogate):
|
|
1684
|
+
return JsonhResult.from_error("Low surrogate out of range")
|
|
1685
|
+
|
|
1686
|
+
return JsonhResult.from_value(0x10000 + (((high_surrogate - 0xD800) << 10) | (low_surrogate - 0xDC00)))
|
|
1550
1687
|
|
|
1551
1688
|
@staticmethod
|
|
1552
1689
|
def _is_utf16_high_surrogate(code_point: int) -> bool:
|
|
1553
1690
|
return code_point >= 0xD800 and code_point <= 0xDBFF
|
|
1554
1691
|
|
|
1692
|
+
@staticmethod
|
|
1693
|
+
def _is_utf16_low_surrogate(code_point: int) -> bool:
|
|
1694
|
+
return code_point >= 0xDC00 and code_point <= 0xDFFF
|
|
1695
|
+
|
|
1555
1696
|
def _peek(self) -> str | None:
|
|
1556
1697
|
if self.index >= len(self.string):
|
|
1557
1698
|
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|