jmux 0.0.3__tar.gz → 0.0.5__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 → jmux-0.0.5}/.github/workflows/ci.yml +1 -4
  2. {jmux-0.0.3 → jmux-0.0.5}/.gitignore +5 -1
  3. {jmux-0.0.3/src/jmux.egg-info → jmux-0.0.5}/PKG-INFO +2 -2
  4. {jmux-0.0.3 → jmux-0.0.5}/pyproject.toml +1 -1
  5. {jmux-0.0.3 → jmux-0.0.5}/src/jmux/awaitable.py +10 -4
  6. {jmux-0.0.3 → jmux-0.0.5}/src/jmux/demux.py +93 -67
  7. {jmux-0.0.3 → jmux-0.0.5}/src/jmux/helpers.py +25 -11
  8. {jmux-0.0.3 → jmux-0.0.5}/src/jmux/pda.py +8 -3
  9. {jmux-0.0.3 → jmux-0.0.5}/src/jmux/types.py +6 -1
  10. {jmux-0.0.3 → jmux-0.0.5/src/jmux.egg-info}/PKG-INFO +2 -2
  11. {jmux-0.0.3 → jmux-0.0.5}/src/jmux.egg-info/SOURCES.txt +0 -1
  12. {jmux-0.0.3 → jmux-0.0.5}/tests/test_demux__parse.py +7 -3
  13. {jmux-0.0.3 → jmux-0.0.5}/tests/test_demux__validate.py +9 -0
  14. {jmux-0.0.3 → jmux-0.0.5}/tests/test_helpers.py +27 -4
  15. jmux-0.0.3/uv.lock +0 -696
  16. {jmux-0.0.3 → jmux-0.0.5}/LICENSE +0 -0
  17. {jmux-0.0.3 → jmux-0.0.5}/README.md +0 -0
  18. {jmux-0.0.3 → jmux-0.0.5}/setup.cfg +0 -0
  19. {jmux-0.0.3 → jmux-0.0.5}/src/jmux/__init__.py +0 -0
  20. {jmux-0.0.3 → jmux-0.0.5}/src/jmux/decoder.py +0 -0
  21. {jmux-0.0.3 → jmux-0.0.5}/src/jmux/error.py +0 -0
  22. {jmux-0.0.3 → jmux-0.0.5}/src/jmux.egg-info/dependency_links.txt +0 -0
  23. {jmux-0.0.3 → jmux-0.0.5}/src/jmux.egg-info/requires.txt +0 -0
  24. {jmux-0.0.3 → jmux-0.0.5}/src/jmux.egg-info/top_level.txt +0 -0
  25. {jmux-0.0.3 → jmux-0.0.5}/tests/conftest.py +0 -0
  26. {jmux-0.0.3 → jmux-0.0.5}/tests/test_awaitables.py +0 -0
  27. {jmux-0.0.3 → jmux-0.0.5}/tests/test_decoder.py +0 -0
  28. {jmux-0.0.3 → jmux-0.0.5}/tests/test_demux__stream.py +0 -0
@@ -8,16 +8,13 @@ on:
8
8
  - "v*.*.*"
9
9
  pull_request: {}
10
10
 
11
- env:
12
- UV_FROZEN: true
13
-
14
11
  jobs:
15
12
  check:
16
13
  runs-on: ubuntu-latest
17
14
  strategy:
18
15
  fail-fast: false
19
16
  matrix:
20
- python-version: ["3.12", "3.13"]
17
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
21
18
 
22
19
  steps:
23
20
  - uses: actions/checkout@v4
@@ -24,6 +24,7 @@ var/
24
24
  *.egg
25
25
 
26
26
  # Virtual environment
27
+ uv.lock
27
28
  venv/
28
29
  ENV/
29
30
  env/
@@ -76,4 +77,7 @@ Thumbs.db
76
77
 
77
78
  # Local project configs
78
79
  .env
79
- .env.*
80
+ .env.*
81
+
82
+ # Cursor
83
+ .cursor/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmux
3
- Version: 0.0.3
3
+ Version: 0.0.5
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,7 +32,7 @@ Project-URL: Repository, https://github.com/jaunruh/jmux
32
32
  Keywords: demultiplexer,python,package,json
33
33
  Classifier: Programming Language :: Python :: 3
34
34
  Classifier: Operating System :: OS Independent
35
- Requires-Python: >=3.12
35
+ Requires-Python: >=3.10
36
36
  Description-Content-Type: text/markdown
37
37
  License-File: LICENSE
38
38
  Requires-Dist: anyio>=4.0.0
@@ -7,7 +7,7 @@ name = "jmux"
7
7
  dynamic = ["version"]
8
8
  description = "JMux: A Python package for demultiplexing a JSON string into multiple awaitable variables."
9
9
  readme = "README.md"
10
- requires-python = ">=3.12"
10
+ requires-python = ">=3.10"
11
11
  classifiers = [
12
12
  "Programming Language :: Python :: 3",
13
13
  "Operating System :: OS Independent",
@@ -1,11 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  from asyncio import Event, Queue
2
4
  from enum import Enum
3
5
  from types import NoneType
4
6
  from typing import (
5
7
  AsyncGenerator,
8
+ Generic,
6
9
  Protocol,
7
10
  Set,
8
11
  Type,
12
+ TypeVar,
9
13
  cast,
10
14
  runtime_checkable,
11
15
  )
@@ -13,13 +17,15 @@ from typing import (
13
17
  from jmux.error import NothingEmittedError, SinkClosedError
14
18
  from jmux.helpers import extract_types_from_generic_alias
15
19
 
20
+ T = TypeVar("T")
21
+
16
22
 
17
23
  class SinkType(Enum):
18
24
  STREAMABLE_VALUES = "StreamableValues"
19
25
  AWAITABLE_VALUE = "AwaitableValue"
20
26
 
21
27
 
22
- class UnderlyingGenericMixin[T]:
28
+ class UnderlyingGenericMixin(Generic[T]):
23
29
  """
24
30
  A mixin class that provides methods for inspecting the generic types of a
25
31
  class at runtime.
@@ -61,7 +67,7 @@ class UnderlyingGenericMixin[T]:
61
67
 
62
68
 
63
69
  @runtime_checkable
64
- class IAsyncSink[T](Protocol):
70
+ class IAsyncSink(Protocol[T]):
65
71
  """
66
72
  An asynchronous sink protocol that defines a common interface for putting, closing,
67
73
  and retrieving values from a sink.
@@ -96,7 +102,7 @@ class IAsyncSink[T](Protocol):
96
102
  ...
97
103
 
98
104
 
99
- class StreamableValues[T](UnderlyingGenericMixin[T]):
105
+ class StreamableValues(UnderlyingGenericMixin[T], Generic[T]):
100
106
  """
101
107
  A class that represents a stream of values that can be asynchronously iterated over.
102
108
  It uses an asyncio.Queue to store the items and allows for putting items into the
@@ -198,7 +204,7 @@ class StreamableValues[T](UnderlyingGenericMixin[T]):
198
204
  yield item
199
205
 
200
206
 
201
- class AwaitableValue[T](UnderlyingGenericMixin[T]):
207
+ class AwaitableValue(UnderlyingGenericMixin[T], Generic[T]):
202
208
  """
203
209
  A class that represents a value that will be available in the future.
204
210
  It can be awaited to get the value, and it can only be set once.
@@ -1,10 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import ABC
2
4
  from enum import Enum
3
5
  from types import NoneType
4
6
  from typing import (
7
+ Generic,
5
8
  Optional,
6
9
  Set,
7
10
  Type,
11
+ TypeVar,
12
+ Union,
8
13
  get_args,
9
14
  get_origin,
10
15
  get_type_hints,
@@ -59,11 +64,13 @@ from jmux.types import (
59
64
  from jmux.types import Mode as M
60
65
  from jmux.types import State as S
61
66
 
62
- type Primitive = int | float | str | bool | None
63
- type Emittable = Primitive | "JMux" | Enum
67
+ Primitive = Union[int, float, str, bool, None]
68
+ Emittable = Union[int, float, str, bool, None, "JMux", Enum]
69
+
70
+ T = TypeVar("T")
64
71
 
65
72
 
66
- class Sink[T: Emittable]:
73
+ class Sink(Generic[T]):
67
74
  def __init__(self, delegate: "JMux"):
68
75
  self._current_key: Optional[str] = None
69
76
  self._current_sink: Optional[IAsyncSink[T]] = None
@@ -216,17 +223,11 @@ class JMux(ABC):
216
223
  pydantic_main_type_set,
217
224
  pydantic_subtype_set,
218
225
  )
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
- )
226
+ cls._assert_correct_set_combinations(
227
+ jmux_main_type_set,
228
+ pydantic_main_type_set,
229
+ pydantic_subtype_set,
230
+ )
230
231
 
231
232
  if StreamableValues in jmux_main_type_set:
232
233
  cls._assert_is_allowed_streamable_values(
@@ -252,6 +253,80 @@ class JMux(ABC):
252
253
  message="Unexpected main type on JMux",
253
254
  )
254
255
 
256
+ @classmethod
257
+ def _assert_correct_set_combinations(
258
+ cls,
259
+ jmux_main_type_set: Set[Type],
260
+ pydantic_main_type_set: Set[Type],
261
+ pydantic_subtype_set: Set[Type],
262
+ ):
263
+ if (
264
+ pydantic_wrong := (
265
+ len(pydantic_main_type_set) != 1 and list not in pydantic_main_type_set
266
+ )
267
+ and len(pydantic_subtype_set) > 0
268
+ ) or len(jmux_main_type_set) != 1:
269
+ wrong_obj = "pydantic" if pydantic_wrong else "JMux"
270
+ wrong_set = pydantic_main_type_set if pydantic_wrong else jmux_main_type_set
271
+ raise ForbiddenTypeHintsError(
272
+ message=(f"Forbidden typing received on {wrong_obj}: {wrong_set}"),
273
+ )
274
+
275
+ @classmethod
276
+ def _assert_only_allowed_types(
277
+ cls,
278
+ jmux_main_type_set: Set[Type],
279
+ jmux_subtype_set: Set[Type],
280
+ pydantic_main_type_set: Set[Type],
281
+ pydantic_subtype_set: Set[Type],
282
+ ) -> None:
283
+ if not all(t in (AwaitableValue, StreamableValues) for t in jmux_main_type_set):
284
+ raise ForbiddenTypeHintsError(
285
+ message=(
286
+ "JMux must have either AwaitableValue or StreamableValues as "
287
+ f"main type, got {jmux_main_type_set}."
288
+ )
289
+ )
290
+
291
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
292
+ set_a=jmux_subtype_set,
293
+ set_b={int, float, str, bool, NoneType, JMux, Enum},
294
+ ):
295
+ raise ForbiddenTypeHintsError(
296
+ message=(
297
+ "JMux sub type must be one of the emittable types, got: "
298
+ f"{jmux_subtype_set}."
299
+ )
300
+ )
301
+
302
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
303
+ set_a=pydantic_subtype_set,
304
+ set_b={int, float, str, bool, NoneType, BaseModel, Enum},
305
+ ):
306
+ raise ForbiddenTypeHintsError(
307
+ message=(
308
+ "Pydantic sub type must be one of the primitive, enum or "
309
+ f"BaseModel, got: {pydantic_subtype_set}."
310
+ )
311
+ )
312
+
313
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
314
+ set_a=pydantic_main_type_set,
315
+ set_b={int, float, str, bool, list, NoneType, BaseModel, Enum},
316
+ ):
317
+ raise ForbiddenTypeHintsError(
318
+ message=(
319
+ "Pydantic main type must be one of the primitive, enum, list "
320
+ f"or BaseModel, got {pydantic_main_type_set}."
321
+ )
322
+ )
323
+
324
+ @classmethod
325
+ def _all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
326
+ cls, set_a: Set[Type], set_b: Set[Type]
327
+ ) -> bool:
328
+ return all(any(issubclass(elem, t) for t in set_b) for elem in set_a)
329
+
255
330
  @classmethod
256
331
  def _assert_is_allowed_streamable_values(
257
332
  cls,
@@ -329,58 +404,6 @@ class JMux(ABC):
329
404
  ),
330
405
  )
331
406
 
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
407
  async def feed_chunks(self, chunks: str) -> None:
385
408
  """
386
409
  Feeds a string of characters to the JMux parser.
@@ -556,7 +579,10 @@ class JMux(ABC):
556
579
  await self._parse_primitive()
557
580
  await self._sink.close()
558
581
  self._decoder.reset()
559
- self._pda.set_state(S.EXPECT_KEY)
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)
560
586
  if ch in OBJECT_CLOSE:
561
587
  await self._finalize()
562
588
  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,7 +1,12 @@
1
- from typing import List
1
+ from __future__ import annotations
2
2
 
3
+ from typing import Generic, List, Optional, TypeVar
3
4
 
4
- class PushDownAutomata[Context, State]:
5
+ Context = TypeVar("Context")
6
+ State = TypeVar("State")
7
+
8
+
9
+ class PushDownAutomata(Generic[Context, State]):
5
10
  def __init__(self, start_state: State) -> None:
6
11
  self._stack: List[Context] = []
7
12
  self._state: State = start_state
@@ -15,7 +20,7 @@ class PushDownAutomata[Context, State]:
15
20
  return self._stack
16
21
 
17
22
  @property
18
- def top(self) -> Context | None:
23
+ def top(self) -> Optional[Context]:
19
24
  if not self._stack:
20
25
  return None
21
26
  return self._stack[-1]
@@ -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.5
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,7 +32,7 @@ Project-URL: Repository, https://github.com/jaunruh/jmux
32
32
  Keywords: demultiplexer,python,package,json
33
33
  Classifier: Programming Language :: Python :: 3
34
34
  Classifier: Operating System :: OS Independent
35
- Requires-Python: >=3.12
35
+ Requires-Python: >=3.10
36
36
  Description-Content-Type: text/markdown
37
37
  License-File: LICENSE
38
38
  Requires-Dist: anyio>=4.0.0
@@ -2,7 +2,6 @@
2
2
  LICENSE
3
3
  README.md
4
4
  pyproject.toml
5
- uv.lock
6
5
  .github/workflows/ci.yml
7
6
  src/jmux/__init__.py
8
7
  src/jmux/awaitable.py
@@ -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})),