jmux 0.0.5__tar.gz → 0.0.6__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.
Files changed (32) hide show
  1. {jmux-0.0.5/src/jmux.egg-info → jmux-0.0.6}/PKG-INFO +1 -1
  2. {jmux-0.0.5 → jmux-0.0.6}/src/jmux/decoder.py +20 -4
  3. {jmux-0.0.5 → jmux-0.0.6}/src/jmux/demux.py +99 -57
  4. {jmux-0.0.5 → jmux-0.0.6}/src/jmux/types.py +5 -0
  5. {jmux-0.0.5 → jmux-0.0.6/src/jmux.egg-info}/PKG-INFO +1 -1
  6. {jmux-0.0.5 → jmux-0.0.6}/src/jmux.egg-info/SOURCES.txt +2 -1
  7. jmux-0.0.6/tests/test_awaitables.py +469 -0
  8. jmux-0.0.6/tests/test_decoder.py +425 -0
  9. {jmux-0.0.5 → jmux-0.0.6}/tests/test_demux__parse.py +991 -13
  10. {jmux-0.0.5 → jmux-0.0.6}/tests/test_demux__stream.py +322 -0
  11. jmux-0.0.6/tests/test_demux__validate.py +627 -0
  12. jmux-0.0.6/tests/test_helpers.py +226 -0
  13. jmux-0.0.6/tests/test_pda.py +266 -0
  14. jmux-0.0.5/tests/test_awaitables.py +0 -78
  15. jmux-0.0.5/tests/test_decoder.py +0 -32
  16. jmux-0.0.5/tests/test_demux__validate.py +0 -134
  17. jmux-0.0.5/tests/test_helpers.py +0 -96
  18. {jmux-0.0.5 → jmux-0.0.6}/.github/workflows/ci.yml +0 -0
  19. {jmux-0.0.5 → jmux-0.0.6}/.gitignore +0 -0
  20. {jmux-0.0.5 → jmux-0.0.6}/LICENSE +0 -0
  21. {jmux-0.0.5 → jmux-0.0.6}/README.md +0 -0
  22. {jmux-0.0.5 → jmux-0.0.6}/pyproject.toml +0 -0
  23. {jmux-0.0.5 → jmux-0.0.6}/setup.cfg +0 -0
  24. {jmux-0.0.5 → jmux-0.0.6}/src/jmux/__init__.py +0 -0
  25. {jmux-0.0.5 → jmux-0.0.6}/src/jmux/awaitable.py +0 -0
  26. {jmux-0.0.5 → jmux-0.0.6}/src/jmux/error.py +0 -0
  27. {jmux-0.0.5 → jmux-0.0.6}/src/jmux/helpers.py +0 -0
  28. {jmux-0.0.5 → jmux-0.0.6}/src/jmux/pda.py +0 -0
  29. {jmux-0.0.5 → jmux-0.0.6}/src/jmux.egg-info/dependency_links.txt +0 -0
  30. {jmux-0.0.5 → jmux-0.0.6}/src/jmux.egg-info/requires.txt +0 -0
  31. {jmux-0.0.5 → jmux-0.0.6}/src/jmux.egg-info/top_level.txt +0 -0
  32. {jmux-0.0.5 → jmux-0.0.6}/tests/conftest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmux
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: JMux: A Python package for demultiplexing a JSON string into multiple awaitable variables.
5
5
  Author-email: "Johannes A.I. Unruh" <johannes@unruh.ai>
6
6
  License: MIT License
@@ -32,26 +32,40 @@ class StringEscapeDecoder:
32
32
  def __init__(self):
33
33
  self._buffer = ""
34
34
  self._string_escape = False
35
+ self._is_parsing_unicode = False
36
+ self._unicode_buffer = ""
35
37
 
36
38
  def push(self, ch: str) -> str | None:
39
+ if self._is_parsing_unicode:
40
+ self._unicode_buffer += ch
41
+ if len(self._unicode_buffer) == 4:
42
+ code_point = int(self._unicode_buffer, 16)
43
+ decoded_char = chr(code_point)
44
+ self._buffer += decoded_char
45
+ self._is_parsing_unicode = False
46
+ self._unicode_buffer = ""
47
+ return decoded_char
48
+ return None
49
+
37
50
  if self._string_escape:
38
51
  self._string_escape = False
39
52
  if ch == "u":
40
- self.is_parsing_unicode = True
41
- self.unicode_buffer = ""
42
- return
53
+ self._is_parsing_unicode = True
54
+ self._unicode_buffer = ""
55
+ return None
43
56
  escaped_char = self.escape_map.get(ch, ch)
44
57
  self._buffer += escaped_char
45
58
  return escaped_char
46
59
 
47
60
  if ch == "\\":
48
61
  self._string_escape = True
62
+ return None
49
63
  else:
50
64
  self._buffer += ch
51
65
  return ch
52
66
 
53
67
  def is_terminating_quote(self, ch: str) -> bool:
54
- if self._string_escape:
68
+ if self._string_escape or self._is_parsing_unicode:
55
69
  return False
56
70
  if ch == '"':
57
71
  return True
@@ -60,6 +74,8 @@ class StringEscapeDecoder:
60
74
  def reset(self) -> None:
61
75
  self._buffer = ""
62
76
  self._string_escape = False
77
+ self._is_parsing_unicode = False
78
+ self._unicode_buffer = ""
63
79
 
64
80
  @property
65
81
  def buffer(self) -> str:
@@ -47,6 +47,8 @@ from jmux.types import (
47
47
  BOOLEAN_OPEN,
48
48
  COLON,
49
49
  COMMA,
50
+ EXPECT_KEY_IN_ROOT,
51
+ EXPECT_VALUE_IN_ARRAY,
50
52
  FLOAT_ALLOWED,
51
53
  INTERGER_ALLOWED,
52
54
  JSON_FALSE,
@@ -199,60 +201,74 @@ class JMux(ABC):
199
201
  ForbiddenTypeHintsError: If a type hint is not allowed.
200
202
  ObjectMissmatchedError: If the JMux class does not match the Pydantic model.
201
203
  """
202
- for attr_name, type_alias in get_type_hints(cls).items():
203
- jmux_main_type_set, jmux_subtype_set = extract_types_from_generic_alias(
204
- type_alias
205
- )
206
-
207
- MaybePydanticType = get_type_hints(pydantic_model).get(attr_name, None)
208
- if MaybePydanticType is None:
209
- if NoneType in jmux_subtype_set:
210
- continue
211
- else:
212
- raise MissingAttributeError(
213
- object_name=pydantic_model.__name__,
214
- attribute=attr_name,
215
- )
204
+ try:
205
+ for attr_name, type_alias in get_type_hints(cls).items():
206
+ jmux_main_type_set, jmux_subtype_set = extract_types_from_generic_alias(
207
+ type_alias
208
+ )
216
209
 
217
- pydantic_main_type_set, pydantic_subtype_set = (
218
- extract_types_from_generic_alias(MaybePydanticType)
219
- )
220
- cls._assert_only_allowed_types(
221
- jmux_main_type_set,
222
- jmux_subtype_set,
223
- pydantic_main_type_set,
224
- pydantic_subtype_set,
225
- )
226
- cls._assert_correct_set_combinations(
227
- jmux_main_type_set,
228
- pydantic_main_type_set,
229
- pydantic_subtype_set,
230
- )
210
+ MaybePydanticType = get_type_hints(pydantic_model).get(attr_name, None)
211
+ if MaybePydanticType is None:
212
+ if NoneType in jmux_subtype_set:
213
+ continue
214
+ else:
215
+ raise MissingAttributeError(
216
+ object_name=pydantic_model.__name__,
217
+ attribute=attr_name,
218
+ )
231
219
 
232
- if StreamableValues in jmux_main_type_set:
233
- cls._assert_is_allowed_streamable_values(
234
- jmux_subtype_set,
235
- pydantic_subtype_set,
236
- pydantic_main_type_set,
237
- pydantic_model,
238
- attr_name,
220
+ pydantic_main_type_set, pydantic_subtype_set = (
221
+ extract_types_from_generic_alias(MaybePydanticType)
239
222
  )
240
- elif AwaitableValue in jmux_main_type_set:
241
- cls._assert_is_allowed_awaitable_value(
223
+ cls._assert_only_allowed_types(
224
+ jmux_main_type_set,
242
225
  jmux_subtype_set,
243
- pydantic_subtype_set,
244
226
  pydantic_main_type_set,
245
- pydantic_model,
246
- attr_name,
227
+ pydantic_subtype_set,
247
228
  )
248
- else:
249
- raise ObjectMissmatchedError(
250
- jmux_model=cls.__name__,
251
- pydantic_model=pydantic_model.__name__,
252
- attribute=attr_name,
253
- message="Unexpected main type on JMux",
229
+ cls._assert_correct_set_combinations(
230
+ jmux_main_type_set,
231
+ pydantic_main_type_set,
232
+ pydantic_subtype_set,
254
233
  )
255
234
 
235
+ if StreamableValues in jmux_main_type_set:
236
+ cls._assert_is_allowed_streamable_values(
237
+ jmux_subtype_set,
238
+ pydantic_subtype_set,
239
+ pydantic_main_type_set,
240
+ pydantic_model,
241
+ attr_name,
242
+ )
243
+ elif AwaitableValue in jmux_main_type_set:
244
+ cls._assert_is_allowed_awaitable_value(
245
+ jmux_subtype_set,
246
+ pydantic_subtype_set,
247
+ pydantic_main_type_set,
248
+ pydantic_model,
249
+ attr_name,
250
+ )
251
+ else:
252
+ raise ObjectMissmatchedError(
253
+ jmux_model=cls.__name__,
254
+ pydantic_model=pydantic_model.__name__,
255
+ attribute=attr_name,
256
+ message="Unexpected main type on JMux",
257
+ )
258
+ except TypeError as e:
259
+ raise ForbiddenTypeHintsError(message=f"Unexpected type hint: {e}") from e
260
+ except MissingAttributeError as e:
261
+ raise e
262
+ except ObjectMissmatchedError as e:
263
+ raise e
264
+ except Exception as e:
265
+ raise ObjectMissmatchedError(
266
+ jmux_model=cls.__name__,
267
+ pydantic_model=pydantic_model.__name__,
268
+ attribute=attr_name,
269
+ message=f"Unexpected main type on JMux: {e}",
270
+ )
271
+
256
272
  @classmethod
257
273
  def _assert_correct_set_combinations(
258
274
  cls,
@@ -481,18 +497,27 @@ class JMux(ABC):
481
497
  # CONTEXT: Root
482
498
  case M.ROOT:
483
499
  match self._pda.state:
484
- case S.EXPECT_KEY:
500
+ case _ if self._pda.state in EXPECT_KEY_IN_ROOT:
485
501
  if ch in JSON_WHITESPACE:
486
502
  pass
487
503
  elif ch == '"':
488
504
  self._pda.set_state(S.PARSING_KEY)
489
505
  self._decoder.reset()
506
+ elif ch in OBJECT_CLOSE:
507
+ if self._pda.state is S.EXPECT_KEY_AFTER_COMMA:
508
+ raise UnexpectedCharacterError(
509
+ ch,
510
+ self._pda.stack,
511
+ self._pda.state,
512
+ "Trailing comma in object is not allowed.",
513
+ )
514
+ await self._finalize()
490
515
  else:
491
516
  raise UnexpectedCharacterError(
492
517
  ch,
493
518
  self._pda.stack,
494
519
  self._pda.state,
495
- "Char needs to be '\"' or JSON whitespaces",
520
+ "Char needs to be '\"', '}' or JSON whitespaces",
496
521
  )
497
522
 
498
523
  case S.PARSING_KEY:
@@ -537,6 +562,14 @@ class JMux(ABC):
537
562
  "Expected '[' or '\"' for 'StreamableValues'",
538
563
  )
539
564
  elif ch in ARRAY_OPEN:
565
+ if self._sink.current_sink_type == SinkType.AWAITABLE_VALUE:
566
+ raise UnexpectedCharacterError(
567
+ ch,
568
+ self._pda.stack,
569
+ self._pda.state,
570
+ "Trying to parse 'array' but sink type is "
571
+ "'AwaitableValue'.",
572
+ )
540
573
  self._pda.set_state(S.EXPECT_VALUE)
541
574
  self._pda.push(M.ARRAY)
542
575
  else:
@@ -579,12 +612,12 @@ class JMux(ABC):
579
612
  await self._parse_primitive()
580
613
  await self._sink.close()
581
614
  self._decoder.reset()
582
- if ch in JSON_WHITESPACE:
583
- self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
584
- else:
585
- self._pda.set_state(S.EXPECT_KEY)
586
- if ch in OBJECT_CLOSE:
615
+ if ch in COMMA:
616
+ self._pda.set_state(S.EXPECT_KEY_AFTER_COMMA)
617
+ elif ch in OBJECT_CLOSE:
587
618
  await self._finalize()
619
+ else:
620
+ self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
588
621
  else:
589
622
  self._assert_primitive_character_allowed_in_state(ch)
590
623
  self._decoder.push(ch)
@@ -593,7 +626,7 @@ class JMux(ABC):
593
626
  if ch in JSON_WHITESPACE:
594
627
  pass
595
628
  elif ch in COMMA:
596
- self._pda.set_state(S.EXPECT_KEY)
629
+ self._pda.set_state(S.EXPECT_KEY_AFTER_COMMA)
597
630
  elif ch in OBJECT_CLOSE:
598
631
  await self._finalize()
599
632
  else:
@@ -622,12 +655,19 @@ class JMux(ABC):
622
655
  )
623
656
 
624
657
  match self._pda.state:
625
- case S.EXPECT_VALUE:
658
+ case _ if self._pda.state in EXPECT_VALUE_IN_ARRAY:
626
659
  if ch in JSON_WHITESPACE:
627
660
  pass
628
661
  elif await self._handle_common__expect_value(ch):
629
662
  pass
630
663
  elif ch in ARRAY_CLOSE:
664
+ if self._pda.state is S.EXPECT_VALUE_AFTER_COMMA:
665
+ raise UnexpectedCharacterError(
666
+ ch,
667
+ self._pda.stack,
668
+ self._pda.state,
669
+ "Trailing comma in array is not allowed.",
670
+ )
631
671
  await self._close_context(S.EXPECT_COMMA_OR_EOC)
632
672
  else:
633
673
  raise UnexpectedCharacterError(
@@ -670,9 +710,11 @@ class JMux(ABC):
670
710
  await self._parse_primitive()
671
711
  self._decoder.reset()
672
712
  if ch in COMMA:
673
- self._pda.set_state(S.EXPECT_VALUE)
713
+ self._pda.set_state(S.EXPECT_VALUE_AFTER_COMMA)
674
714
  elif ch in ARRAY_CLOSE:
675
715
  await self._close_context(S.EXPECT_COMMA_OR_EOC)
716
+ else:
717
+ self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
676
718
  else:
677
719
  self._assert_primitive_character_allowed_in_state(ch)
678
720
  self._decoder.push(ch)
@@ -681,7 +723,7 @@ class JMux(ABC):
681
723
  if ch in JSON_WHITESPACE:
682
724
  pass
683
725
  elif ch in COMMA:
684
- self._pda.set_state(S.EXPECT_VALUE)
726
+ self._pda.set_state(S.EXPECT_VALUE_AFTER_COMMA)
685
727
  elif ch in ARRAY_CLOSE:
686
728
  await self._close_context(S.EXPECT_COMMA_OR_EOC)
687
729
  else:
@@ -9,8 +9,10 @@ class State(Enum):
9
9
  ERROR = "error"
10
10
  # expect
11
11
  EXPECT_KEY = "expect_key"
12
+ EXPECT_KEY_AFTER_COMMA = "expect_key_after_comma"
12
13
  EXPECT_COLON = "expect_colon"
13
14
  EXPECT_VALUE = "expect_value"
15
+ EXPECT_VALUE_AFTER_COMMA = "expect_value_after_comma"
14
16
  EXPECT_COMMA_OR_EOC = "expect_comma_or_eoc"
15
17
  # parsing
16
18
  PARSING_KEY = "parsing_key"
@@ -29,6 +31,9 @@ PARSING_PRIMITIVE_STATES: Set[State] = {
29
31
  State.PARSING_NULL,
30
32
  }
31
33
 
34
+ EXPECT_KEY_IN_ROOT = {State.EXPECT_KEY, State.EXPECT_KEY_AFTER_COMMA}
35
+ EXPECT_VALUE_IN_ARRAY = {State.EXPECT_VALUE, State.EXPECT_VALUE_AFTER_COMMA}
36
+
32
37
 
33
38
  class Mode(Enum):
34
39
  ROOT = "$"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmux
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: JMux: A Python package for demultiplexing a JSON string into multiple awaitable variables.
5
5
  Author-email: "Johannes A.I. Unruh" <johannes@unruh.ai>
6
6
  License: MIT License
@@ -22,4 +22,5 @@ tests/test_decoder.py
22
22
  tests/test_demux__parse.py
23
23
  tests/test_demux__stream.py
24
24
  tests/test_demux__validate.py
25
- tests/test_helpers.py
25
+ tests/test_helpers.py
26
+ tests/test_pda.py