jmux 0.0.3__tar.gz → 0.0.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.
Files changed (28) hide show
  1. {jmux-0.0.3/src/jmux.egg-info → jmux-0.0.4}/PKG-INFO +1 -1
  2. {jmux-0.0.3 → jmux-0.0.4}/src/jmux/demux.py +83 -64
  3. {jmux-0.0.3 → jmux-0.0.4}/src/jmux/helpers.py +25 -11
  4. {jmux-0.0.3 → jmux-0.0.4}/src/jmux/types.py +6 -1
  5. {jmux-0.0.3 → jmux-0.0.4/src/jmux.egg-info}/PKG-INFO +1 -1
  6. {jmux-0.0.3 → jmux-0.0.4}/tests/test_demux__parse.py +7 -3
  7. {jmux-0.0.3 → jmux-0.0.4}/tests/test_demux__validate.py +9 -0
  8. {jmux-0.0.3 → jmux-0.0.4}/tests/test_helpers.py +27 -4
  9. {jmux-0.0.3 → jmux-0.0.4}/.github/workflows/ci.yml +0 -0
  10. {jmux-0.0.3 → jmux-0.0.4}/.gitignore +0 -0
  11. {jmux-0.0.3 → jmux-0.0.4}/LICENSE +0 -0
  12. {jmux-0.0.3 → jmux-0.0.4}/README.md +0 -0
  13. {jmux-0.0.3 → jmux-0.0.4}/pyproject.toml +0 -0
  14. {jmux-0.0.3 → jmux-0.0.4}/setup.cfg +0 -0
  15. {jmux-0.0.3 → jmux-0.0.4}/src/jmux/__init__.py +0 -0
  16. {jmux-0.0.3 → jmux-0.0.4}/src/jmux/awaitable.py +0 -0
  17. {jmux-0.0.3 → jmux-0.0.4}/src/jmux/decoder.py +0 -0
  18. {jmux-0.0.3 → jmux-0.0.4}/src/jmux/error.py +0 -0
  19. {jmux-0.0.3 → jmux-0.0.4}/src/jmux/pda.py +0 -0
  20. {jmux-0.0.3 → jmux-0.0.4}/src/jmux.egg-info/SOURCES.txt +0 -0
  21. {jmux-0.0.3 → jmux-0.0.4}/src/jmux.egg-info/dependency_links.txt +0 -0
  22. {jmux-0.0.3 → jmux-0.0.4}/src/jmux.egg-info/requires.txt +0 -0
  23. {jmux-0.0.3 → jmux-0.0.4}/src/jmux.egg-info/top_level.txt +0 -0
  24. {jmux-0.0.3 → jmux-0.0.4}/tests/conftest.py +0 -0
  25. {jmux-0.0.3 → jmux-0.0.4}/tests/test_awaitables.py +0 -0
  26. {jmux-0.0.3 → jmux-0.0.4}/tests/test_decoder.py +0 -0
  27. {jmux-0.0.3 → jmux-0.0.4}/tests/test_demux__stream.py +0 -0
  28. {jmux-0.0.3 → jmux-0.0.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmux
3
- Version: 0.0.3
3
+ Version: 0.0.4
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
@@ -216,17 +216,11 @@ class JMux(ABC):
216
216
  pydantic_main_type_set,
217
217
  pydantic_subtype_set,
218
218
  )
219
- if (
220
- pydantic_wrong := len(pydantic_main_type_set) != 1
221
- and len(pydantic_subtype_set) > 0
222
- ) or len(jmux_main_type_set) != 1:
223
- wrong_obj = "pydantic" if pydantic_wrong else "JMux"
224
- wrong_set = (
225
- jmux_main_type_set if pydantic_wrong else pydantic_main_type_set
226
- )
227
- raise ForbiddenTypeHintsError(
228
- message=(f"Forbidden typing received on {wrong_obj}: {wrong_set}"),
229
- )
219
+ cls._assert_correct_set_combinations(
220
+ jmux_main_type_set,
221
+ pydantic_main_type_set,
222
+ pydantic_subtype_set,
223
+ )
230
224
 
231
225
  if StreamableValues in jmux_main_type_set:
232
226
  cls._assert_is_allowed_streamable_values(
@@ -252,6 +246,80 @@ class JMux(ABC):
252
246
  message="Unexpected main type on JMux",
253
247
  )
254
248
 
249
+ @classmethod
250
+ def _assert_correct_set_combinations(
251
+ cls,
252
+ jmux_main_type_set: Set[Type],
253
+ pydantic_main_type_set: Set[Type],
254
+ pydantic_subtype_set: Set[Type],
255
+ ):
256
+ if (
257
+ pydantic_wrong := (
258
+ len(pydantic_main_type_set) != 1 and list not in pydantic_main_type_set
259
+ )
260
+ and len(pydantic_subtype_set) > 0
261
+ ) or len(jmux_main_type_set) != 1:
262
+ wrong_obj = "pydantic" if pydantic_wrong else "JMux"
263
+ wrong_set = pydantic_main_type_set if pydantic_wrong else jmux_main_type_set
264
+ raise ForbiddenTypeHintsError(
265
+ message=(f"Forbidden typing received on {wrong_obj}: {wrong_set}"),
266
+ )
267
+
268
+ @classmethod
269
+ def _assert_only_allowed_types(
270
+ cls,
271
+ jmux_main_type_set: Set[Type],
272
+ jmux_subtype_set: Set[Type],
273
+ pydantic_main_type_set: Set[Type],
274
+ pydantic_subtype_set: Set[Type],
275
+ ) -> None:
276
+ if not all(t in (AwaitableValue, StreamableValues) for t in jmux_main_type_set):
277
+ raise ForbiddenTypeHintsError(
278
+ message=(
279
+ "JMux must have either AwaitableValue or StreamableValues as "
280
+ f"main type, got {jmux_main_type_set}."
281
+ )
282
+ )
283
+
284
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
285
+ set_a=jmux_subtype_set,
286
+ set_b={int, float, str, bool, NoneType, JMux, Enum},
287
+ ):
288
+ raise ForbiddenTypeHintsError(
289
+ message=(
290
+ "JMux sub type must be one of the emittable types, got: "
291
+ f"{jmux_subtype_set}."
292
+ )
293
+ )
294
+
295
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
296
+ set_a=pydantic_subtype_set,
297
+ set_b={int, float, str, bool, NoneType, BaseModel, Enum},
298
+ ):
299
+ raise ForbiddenTypeHintsError(
300
+ message=(
301
+ "Pydantic sub type must be one of the primitive, enum or "
302
+ f"BaseModel, got: {pydantic_subtype_set}."
303
+ )
304
+ )
305
+
306
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
307
+ set_a=pydantic_main_type_set,
308
+ set_b={int, float, str, bool, list, NoneType, BaseModel, Enum},
309
+ ):
310
+ raise ForbiddenTypeHintsError(
311
+ message=(
312
+ "Pydantic main type must be one of the primitive, enum, list "
313
+ f"or BaseModel, got {pydantic_main_type_set}."
314
+ )
315
+ )
316
+
317
+ @classmethod
318
+ def _all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
319
+ cls, set_a: Set[Type], set_b: Set[Type]
320
+ ) -> bool:
321
+ return all(any(issubclass(elem, t) for t in set_b) for elem in set_a)
322
+
255
323
  @classmethod
256
324
  def _assert_is_allowed_streamable_values(
257
325
  cls,
@@ -329,58 +397,6 @@ class JMux(ABC):
329
397
  ),
330
398
  )
331
399
 
332
- @classmethod
333
- def _assert_only_allowed_types(
334
- cls,
335
- jmux_main_type_set: Set[Type],
336
- jmux_subtype_set: Set[Type],
337
- pydantic_main_type_set: Set[Type],
338
- pydantic_subtype_set: Set[Type],
339
- ) -> None:
340
- if not all(t in (AwaitableValue, StreamableValues) for t in jmux_main_type_set):
341
- raise ForbiddenTypeHintsError(
342
- message=(
343
- "JMux must have either AwaitableValue or StreamableValues as "
344
- f"main type, got {jmux_main_type_set}."
345
- )
346
- )
347
-
348
- if not any(
349
- issubclass(elem, t)
350
- for t in (int, float, str, bool, NoneType, JMux, Enum)
351
- for elem in jmux_subtype_set
352
- ):
353
- raise ForbiddenTypeHintsError(
354
- message=(
355
- "JMux sub type must be one of the emittable types: "
356
- f"{jmux_subtype_set}."
357
- )
358
- )
359
-
360
- if len(pydantic_subtype_set) > 0 and not any(
361
- issubclass(elem, t)
362
- for t in (int, float, str, bool, NoneType, BaseModel, Enum)
363
- for elem in pydantic_subtype_set
364
- ):
365
- raise ForbiddenTypeHintsError(
366
- message=(
367
- "Pydantic sub type must be one of the primitive, enum or "
368
- f"BaseModel, got: {pydantic_subtype_set}."
369
- )
370
- )
371
-
372
- if not any(
373
- issubclass(elem, t)
374
- for t in (int, float, str, bool, list, NoneType, BaseModel, Enum)
375
- for elem in pydantic_main_type_set
376
- ):
377
- raise ForbiddenTypeHintsError(
378
- message=(
379
- "Pydantic main type must be one of the primitive, enum, list "
380
- f"or BaseModel, got {pydantic_main_type_set}."
381
- )
382
- )
383
-
384
400
  async def feed_chunks(self, chunks: str) -> None:
385
401
  """
386
402
  Feeds a string of characters to the JMux parser.
@@ -556,7 +572,10 @@ class JMux(ABC):
556
572
  await self._parse_primitive()
557
573
  await self._sink.close()
558
574
  self._decoder.reset()
559
- self._pda.set_state(S.EXPECT_KEY)
575
+ if ch in JSON_WHITESPACE:
576
+ self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
577
+ else:
578
+ self._pda.set_state(S.EXPECT_KEY)
560
579
  if ch in OBJECT_CLOSE:
561
580
  await self._finalize()
562
581
  else:
@@ -1,7 +1,8 @@
1
- from types import NoneType, UnionType
2
- from typing import Set, Tuple, Type, Union, get_args, get_origin
1
+ from types import NoneType
2
+ from typing import Set, Tuple, Type, get_args, get_origin
3
3
 
4
4
  from jmux.error import ParsePrimitiveError
5
+ from jmux.types import TYPES_LIKE_NONE, TYPES_LIKE_UNION
5
6
 
6
7
 
7
8
  def str_to_bool(s: str) -> bool:
@@ -20,8 +21,17 @@ def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[
20
21
  Origin: Type | None = get_origin(UnknownType)
21
22
  if Origin is None:
22
23
  return {UnknownType}, set()
23
- if Origin is UnionType or Origin is Union:
24
- return deconstruct_type(UnknownType), set()
24
+ if Origin in TYPES_LIKE_UNION:
25
+ deconstructed = deconstruct_flat_type(UnknownType)
26
+ maybe_list_types = [
27
+ subtypes for subtypes in deconstructed if get_origin(subtypes) is list
28
+ ]
29
+ if len(maybe_list_types) == 1:
30
+ list_based_type = maybe_list_types[0]
31
+ non_list_types = deconstructed - {list_based_type}
32
+ main_type, subtype = extract_types_from_generic_alias(list_based_type)
33
+ return non_list_types | main_type, subtype
34
+ return deconstructed, set()
25
35
 
26
36
  type_args = get_args(UnknownType)
27
37
  if len(type_args) != 1:
@@ -31,7 +41,7 @@ def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[
31
41
  )
32
42
 
33
43
  Generic: Type = type_args[0]
34
- type_set = deconstruct_type(Generic)
44
+ type_set = deconstruct_flat_type(Generic)
35
45
  if len(type_set) == 1:
36
46
  return {Origin}, type_set
37
47
  if len(type_set) != 2:
@@ -46,16 +56,20 @@ def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[
46
56
  return {Origin}, type_set
47
57
 
48
58
 
49
- def deconstruct_type(UnknownType: Type) -> Set[Type]:
59
+ def deconstruct_flat_type(UnknownType: Type) -> Set[Type]:
50
60
  Origin: Type | None = get_origin(UnknownType)
51
- if UnknownType is None:
61
+ if UnknownType in TYPES_LIKE_NONE:
52
62
  return {NoneType}
53
63
  if Origin is None:
54
64
  return {UnknownType}
55
- if not (Origin is UnionType or Origin is Union):
56
- return {Origin}
57
- type_args = get_args(UnknownType)
58
- return set(type_args)
65
+ if Origin in TYPES_LIKE_UNION:
66
+ type_args = get_args(UnknownType)
67
+ return set(type_args)
68
+ raise TypeError(
69
+ f"Unknown type {UnknownType} is not a Union or optional type, "
70
+ "only only those types and their syntactic sugar are supported "
71
+ "for flat deconstruction."
72
+ )
59
73
 
60
74
 
61
75
  def get_main_type(type_set: Set[Type]) -> Type:
@@ -1,5 +1,6 @@
1
1
  from enum import Enum
2
- from typing import Set
2
+ from types import NoneType, UnionType
3
+ from typing import List, Set, Union
3
4
 
4
5
 
5
6
  class State(Enum):
@@ -56,3 +57,7 @@ JSON_FALSE = "false"
56
57
  JSON_TRUE = "true"
57
58
  JSON_NULL = "null"
58
59
  JSON_WHITESPACE = set(" \t\n\r")
60
+
61
+ TYPES_LIKE_UNION = {UnionType, Union}
62
+ TYPES_LIKE_NONE = {NoneType, None}
63
+ TYPES_LIKE_LIST = {List, list}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmux
3
- Version: 0.0.3
3
+ Version: 0.0.4
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
@@ -282,7 +282,7 @@ parse_incorrect_stream__params = [
282
282
  parse_incorrect_stream__params,
283
283
  )
284
284
  @pytest.mark.anyio
285
- async def test_json_demux__parse_incorrect_stream__assert_error(
285
+ async def test_json_demux__parse_stream__assert_error(
286
286
  stream: str, MaybeExpectedError: Type[Exception] | None
287
287
  ):
288
288
  class SObject(JMux):
@@ -375,7 +375,7 @@ parse_incorrect_stream_with_optionals__params = [
375
375
  parse_incorrect_stream_with_optionals__params,
376
376
  )
377
377
  @pytest.mark.anyio
378
- async def test_json_demux__parse_incorrect_stream_with_optionals__assert_error(
378
+ async def test_json_demux__parse_stream_with_optionals__assert_error(
379
379
  stream: str, MaybeExpectedError: Type[Exception] | None
380
380
  ):
381
381
  class SObject(JMux):
@@ -416,6 +416,10 @@ parse_correct_stream__double_nested__params = [
416
416
  ('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": "val', None),
417
417
  ('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": "val"}', None),
418
418
  ('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": "val"}}', None),
419
+ ('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": null}}', None),
420
+ ('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": null\n}}', None),
421
+ ('{"key_first_nested": null}', None),
422
+ ('{"key_first_nested": null\n}', None),
419
423
  ]
420
424
  # fmt: on
421
425
  @pytest.mark.parametrize(
@@ -423,7 +427,7 @@ parse_correct_stream__double_nested__params = [
423
427
  parse_correct_stream__double_nested__params,
424
428
  )
425
429
  @pytest.mark.anyio
426
- async def test_json_demux__parse_correct_stream__double_nested(
430
+ async def test_json_demux__parse_stream__double_nested(
427
431
  stream: str, MaybeExpectedError: Type[Exception] | None
428
432
  ):
429
433
  class SObject(JMux):
@@ -72,6 +72,14 @@ class CorrectPydantic_2(BaseModel):
72
72
  key_bool: bool | None
73
73
 
74
74
 
75
+ class CorrectJMux_3(JMux):
76
+ arr_str: StreamableValues[str]
77
+
78
+
79
+ class CorrectPydantic_3(BaseModel):
80
+ arr_str: list[str] | None
81
+
82
+
75
83
  class IncorrectJMux_1(JMux):
76
84
  key_str: AwaitableValue[str]
77
85
 
@@ -107,6 +115,7 @@ class IncorrectPydantic_3(BaseModel):
107
115
  [
108
116
  (CorrectJMux_1, CorrectPydantic_1, None),
109
117
  (CorrectJMux_2, CorrectPydantic_2, None),
118
+ (CorrectJMux_3, CorrectPydantic_3, None),
110
119
  (IncorrectJMux_1, IncorrectPydantic_1, ObjectMissmatchedError),
111
120
  (IncorrectJMux_2, IncorrectPydantic_2, ObjectMissmatchedError),
112
121
  (IncorrectJMux_3, IncorrectPydantic_3, ObjectMissmatchedError),
@@ -1,5 +1,5 @@
1
1
  from types import NoneType
2
- from typing import List, Optional, Set, Tuple, Type
2
+ from typing import List, Optional, Set, Tuple, Type, Union
3
3
 
4
4
  import pytest
5
5
 
@@ -9,26 +9,43 @@ from jmux.awaitable import (
9
9
  UnderlyingGenericMixin,
10
10
  )
11
11
  from jmux.demux import JMux
12
- from jmux.helpers import deconstruct_type, extract_types_from_generic_alias
12
+ from jmux.helpers import deconstruct_flat_type, extract_types_from_generic_alias
13
13
 
14
14
 
15
15
  @pytest.mark.parametrize(
16
16
  "TargetType,expected_tuple",
17
17
  [
18
18
  (int, {int}),
19
+ (Union[int], {int}),
20
+ #
19
21
  (Optional[int], {int, NoneType}),
20
22
  (int | None, {int, NoneType}),
23
+ #
24
+ (Union[int, None], {int, NoneType}),
25
+ (int | NoneType, {int, NoneType}),
26
+ (Union[int, NoneType], {int, NoneType}),
27
+ #
21
28
  (int | str, {str, int}),
29
+ (Union[int, str], {str, int}),
30
+ #
22
31
  (int | str | NoneType, {str, int, NoneType}),
32
+ (Union[int, str, NoneType], {str, int, NoneType}),
33
+ #
23
34
  (JMux, {JMux}),
35
+ (Union[JMux], {JMux}),
36
+ #
24
37
  (JMux | None, {JMux, NoneType}),
38
+ (Union[JMux, None], {JMux, NoneType}),
39
+ (Union[JMux, NoneType], {JMux, NoneType}),
40
+ #
25
41
  (JMux | NoneType, {JMux, NoneType}),
42
+ (Union[JMux, NoneType], {JMux, NoneType}),
26
43
  ],
27
44
  )
28
- def test_extract_types(
45
+ def test_deconstruct_flat_types(
29
46
  TargetType: Type[UnderlyingGenericMixin], expected_tuple: Tuple[Type, Set[Type]]
30
47
  ):
31
- underlying_types = deconstruct_type(TargetType)
48
+ underlying_types = deconstruct_flat_type(TargetType)
32
49
 
33
50
  assert underlying_types == expected_tuple
34
51
 
@@ -46,7 +63,13 @@ class NestedObject(JMux):
46
63
  (Optional[int], ({int, NoneType}, set())),
47
64
  (int | None, ({int, NoneType}, set())),
48
65
  (List[int], ({list}, {int})),
66
+ (list[int], ({list}, {int})),
49
67
  (List[int | None], ({list}, {int, NoneType})),
68
+ (list[int | None], ({list}, {int, NoneType})),
69
+ (List[int] | None, ({list, NoneType}, {int})),
70
+ (list[int] | None, ({list, NoneType}, {int})),
71
+ (Optional[List[int]], ({list, NoneType}, {int})),
72
+ (Optional[list[int]], ({list, NoneType}, {int})),
50
73
  (AwaitableValue[int], ({AwaitableValue}, {int})),
51
74
  (AwaitableValue[float], ({AwaitableValue}, {float})),
52
75
  (AwaitableValue[str], ({AwaitableValue}, {str})),
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes