mode-streaming 0.5.1__tar.gz → 0.5.3__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 (164) hide show
  1. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/.github/workflows/tests.yml +1 -3
  2. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/PKG-INFO +2 -1
  3. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/loops.py +28 -2
  4. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/objects.py +37 -6
  5. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode_streaming.egg-info/PKG-INFO +2 -1
  6. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode_streaming.egg-info/scm_version.json +2 -2
  7. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/pyproject.toml +1 -0
  8. mode_streaming-0.5.3/tests/unit/utils/test_loops.py +94 -0
  9. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/utils/test_objects.py +32 -0
  10. mode_streaming-0.5.1/tests/unit/utils/test_loops.py +0 -48
  11. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/.editorconfig +0 -0
  12. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/.github/ISSUE_TEMPLATE.md +0 -0
  13. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/.github/workflows/deploy-docs.yml +0 -0
  15. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/.github/workflows/publish.yml +0 -0
  16. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/.gitignore +0 -0
  17. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/.pre-commit-config.yaml +0 -0
  18. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/AUTHORS +0 -0
  19. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/CHANGELOG.md +0 -0
  20. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/CODE_OF_CONDUCT.md +0 -0
  21. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/CONTRIBUTING.md +0 -0
  22. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/LICENSE +0 -0
  23. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/README.md +0 -0
  24. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/changelog.md +0 -0
  25. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/contributing.md +0 -0
  26. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/creating-service.md +0 -0
  27. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/example-service.md +0 -0
  28. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/example-webapp.md +0 -0
  29. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/index.md +0 -0
  30. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.debug.md +0 -0
  31. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.exceptions.md +0 -0
  32. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.locals.md +0 -0
  33. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.loop.eventlet.md +0 -0
  34. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.loop.gevent.md +0 -0
  35. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.loop.md +0 -0
  36. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.loop.uvloop.md +0 -0
  37. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.proxy.md +0 -0
  38. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.services.md +0 -0
  39. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.signals.md +0 -0
  40. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.supervisors.md +0 -0
  41. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.threads.md +0 -0
  42. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.timers.md +0 -0
  43. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.types.md +0 -0
  44. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.types.services.md +0 -0
  45. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.types.signals.md +0 -0
  46. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.types.supervisors.md +0 -0
  47. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.aiter.md +0 -0
  48. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.collections.md +0 -0
  49. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.compat.md +0 -0
  50. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.contexts.md +0 -0
  51. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.cron.md +0 -0
  52. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.futures.md +0 -0
  53. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.graphs.md +0 -0
  54. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.imports.md +0 -0
  55. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.locals.md +0 -0
  56. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.locks.md +0 -0
  57. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.logging.md +0 -0
  58. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.loops.md +0 -0
  59. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.mocks.md +0 -0
  60. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.objects.md +0 -0
  61. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.queues.md +0 -0
  62. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.text.md +0 -0
  63. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.times.md +0 -0
  64. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.tracebacks.md +0 -0
  65. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.trees.md +0 -0
  66. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.types.graphs.md +0 -0
  67. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.utils.types.trees.md +0 -0
  68. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/docs/references/mode.worker.md +0 -0
  69. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/examples/service.py +0 -0
  70. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/examples/webapp.py +0 -0
  71. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mkdocs.yml +0 -0
  72. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/__init__.py +0 -0
  73. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/debug.py +0 -0
  74. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/exceptions.py +0 -0
  75. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/locals.py +0 -0
  76. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/loop/__init__.py +0 -0
  77. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/loop/_gevent_loop.py +0 -0
  78. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/loop/eventlet.py +0 -0
  79. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/loop/gevent.py +0 -0
  80. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/loop/uvloop.py +0 -0
  81. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/proxy.py +0 -0
  82. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/py.typed +0 -0
  83. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/services.py +0 -0
  84. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/signals.py +0 -0
  85. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/supervisors.py +0 -0
  86. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/threads.py +0 -0
  87. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/timers.py +0 -0
  88. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/types/__init__.py +0 -0
  89. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/types/services.py +0 -0
  90. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/types/signals.py +0 -0
  91. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/types/supervisors.py +0 -0
  92. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/__init__.py +0 -0
  93. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/aiter.py +0 -0
  94. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/collections.py +0 -0
  95. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/compat.py +0 -0
  96. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/contexts.py +0 -0
  97. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/cron.py +0 -0
  98. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/futures.py +0 -0
  99. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/graphs/__init__.py +0 -0
  100. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/graphs/formatter.py +0 -0
  101. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/graphs/graph.py +0 -0
  102. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/imports.py +0 -0
  103. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/locals.py +0 -0
  104. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/locks.py +0 -0
  105. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/logging.py +0 -0
  106. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/mocks.py +0 -0
  107. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/queues.py +0 -0
  108. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/text.py +0 -0
  109. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/times.py +0 -0
  110. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/tracebacks.py +0 -0
  111. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/trees.py +0 -0
  112. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/types/__init__.py +0 -0
  113. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/types/graphs.py +0 -0
  114. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/utils/types/trees.py +0 -0
  115. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode/worker.py +0 -0
  116. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode_streaming.egg-info/SOURCES.txt +0 -0
  117. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode_streaming.egg-info/dependency_links.txt +0 -0
  118. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode_streaming.egg-info/requires.txt +0 -0
  119. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode_streaming.egg-info/scm_file_list.json +0 -0
  120. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/mode_streaming.egg-info/top_level.txt +0 -0
  121. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/requirements-docs.txt +0 -0
  122. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/requirements-tests.txt +0 -0
  123. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/requirements.txt +0 -0
  124. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/scripts/build-docs.sh +0 -0
  125. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/scripts/build.sh +0 -0
  126. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/scripts/bump.sh +0 -0
  127. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/scripts/clean.sh +0 -0
  128. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/scripts/format.sh +0 -0
  129. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/scripts/lint.sh +0 -0
  130. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/scripts/tests.sh +0 -0
  131. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/setup.cfg +0 -0
  132. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/__init__.py +0 -0
  133. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/conftest.py +0 -0
  134. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/__init__.py +0 -0
  135. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/test_mode.py +0 -0
  136. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/test_proxy.py +0 -0
  137. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/test_service.py +0 -0
  138. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/test_signals.py +0 -0
  139. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/test_supervisors.py +0 -0
  140. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/test_timers.py +0 -0
  141. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/__init__.py +0 -0
  142. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_aiter.py +0 -0
  143. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_collections.py +0 -0
  144. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_compat.py +0 -0
  145. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_contexts.py +0 -0
  146. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_futures.py +0 -0
  147. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_locals.py +0 -0
  148. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_queues.py +0 -0
  149. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_text.py +0 -0
  150. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_times.py +0 -0
  151. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_tracebacks.py +0 -0
  152. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/functional/utils/test_trees.py +0 -0
  153. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/__init__.py +0 -0
  154. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/test_debug.py +0 -0
  155. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/test_locals.py +0 -0
  156. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/test_services.py +0 -0
  157. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/test_supervisors.py +0 -0
  158. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/test_threads.py +0 -0
  159. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/test_worker.py +0 -0
  160. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/utils/__init__.py +0 -0
  161. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/utils/test_cron.py +0 -0
  162. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/utils/test_imports.py +0 -0
  163. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/utils/test_locks.py +0 -0
  164. {mode_streaming-0.5.1 → mode_streaming-0.5.3}/tests/unit/utils/test_logging.py +0 -0
@@ -26,10 +26,8 @@ jobs:
26
26
  - "3.11"
27
27
  - "3.12"
28
28
  - "3.13"
29
+ - "3.14"
29
30
  experimental: [ false ]
30
- include:
31
- - python-version: "~3.14.0-0"
32
- experimental: true
33
31
 
34
32
  steps:
35
33
  - uses: actions/checkout@v3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mode-streaming
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: AsyncIO Service-based programming
5
5
  Author-email: Ask Solem Hoel <ask@robinhood.com>
6
6
  Maintainer-email: William Barnhart <williambbarnhart@gmail.com>
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
20
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
22
  Classifier: Programming Language :: Python :: Implementation :: PyPy
22
23
  Classifier: Operating System :: POSIX
@@ -1,10 +1,19 @@
1
1
  """Event loop utilities."""
2
2
 
3
3
  import asyncio
4
+ import threading
4
5
  from typing import Any, Callable, Optional
5
6
 
6
7
  __all__ = ["call_asap", "clone_loop", "get_event_loop"]
7
8
 
9
+ #: Per-thread cache of the loop resolved by :func:`get_event_loop` for when
10
+ #: there is no *running* loop. ``threading.local`` gives every thread its
11
+ #: own attribute storage for free (no cross-thread leakage, no explicit
12
+ #: cleanup required when a thread exits) -- required for correctness, not
13
+ #: just performance, since Mode creates and runs a dedicated event loop per
14
+ #: :class:`~mode.threads.ServiceThread` worker thread.
15
+ _current_loop = threading.local()
16
+
8
17
 
9
18
  def get_event_loop() -> asyncio.AbstractEventLoop:
10
19
  """Return the current event loop, creating one if necessary.
@@ -20,17 +29,34 @@ def get_event_loop() -> asyncio.AbstractEventLoop:
20
29
  loop -- e.g. at import time, when agents/services are declared at module
21
30
  level -- so it needs the historical "get or create" semantics. This
22
31
  restores them in a way that works across Python 3.9-3.14.
32
+
33
+ Whether a loop is currently *running* can change on every call (that's
34
+ the whole point of an event loop), so :func:`asyncio.get_running_loop`
35
+ must always be re-checked and can't be cached. When nothing is running,
36
+ though, the resolved loop is stable for the lifetime of the thread that
37
+ resolved it, so it's cached in thread-local storage: once a thread has
38
+ created (or found) its loop, later calls on that thread return the
39
+ cached reference directly instead of re-running the
40
+ ``asyncio.get_event_loop()``-raises-then-create dance every time.
23
41
  """
24
42
  try:
25
43
  return asyncio.get_running_loop()
26
44
  except RuntimeError:
27
45
  pass
46
+
47
+ loop: Optional[asyncio.AbstractEventLoop] = getattr(
48
+ _current_loop, "loop", None
49
+ )
50
+ if loop is not None and not loop.is_closed():
51
+ return loop
52
+
28
53
  try:
29
- return asyncio.get_event_loop()
54
+ loop = asyncio.get_event_loop()
30
55
  except RuntimeError:
31
56
  loop = asyncio.new_event_loop()
32
57
  asyncio.set_event_loop(loop)
33
- return loop
58
+ _current_loop.loop = loop
59
+ return loop
34
60
 
35
61
 
36
62
  def _is_unix_loop(loop: asyncio.AbstractEventLoop) -> bool:
@@ -366,11 +366,18 @@ def annotations(
366
366
  )
367
367
  )
368
368
 
369
- # Normalize all field types for forward refs
370
- normalized_fields = {
371
- k: _normalize_forwardref(v) for k, v in fields.items()
372
- }
373
- return normalized_fields, defaults
369
+ # NOTE: Return the resolved field types as-is. `_resolve_refs`/`eval_type`
370
+ # above already turned string/ForwardRef annotations into real types, so
371
+ # nothing further is needed here. Do NOT run `_normalize_forwardref` over
372
+ # the result: it rewrites any type whose ``__qualname__`` contains
373
+ # "<locals>" (i.e. every class defined inside a function) down to a bare
374
+ # ``__name__`` *string*, destroying the actual class object. Callers such
375
+ # as faust.Record rely on identity of the returned types (e.g. matching a
376
+ # field type against a custom coercion mapping), which silently breaks
377
+ # when the type is replaced by its name string. `_normalize_forwardref`
378
+ # remains available as a comparison helper for callers/tests that want a
379
+ # canonical, hashable-by-name form.
380
+ return fields, defaults
374
381
 
375
382
 
376
383
  def local_annotations(
@@ -453,6 +460,7 @@ def eval_type(
453
460
  """
454
461
  invalid_types = invalid_types or set()
455
462
  alias_types = alias_types or {}
463
+ original_str = typ if isinstance(typ, str) else None
456
464
  if isinstance(typ, str):
457
465
  typ = ForwardRef(typ)
458
466
  # `typing._eval_type` (imported above as `_eval_type`) already resolves
@@ -465,7 +473,30 @@ def eval_type(
465
473
  # attributes are not part of any stable API and are no longer present at
466
474
  # all on Python 3.14's ForwardRef, which raised AttributeError. mode's
467
475
  # floor is Python 3.9, well past the versions that workaround targeted).
468
- typ = _eval_type(typ, globalns, localns)
476
+ if isinstance(typ, ForwardRef):
477
+ try:
478
+ typ = _eval_type(typ, globalns, localns)
479
+ except TypeError:
480
+ # `_eval_type` runs the forward ref through `typing._type_check`,
481
+ # which rejects `ClassVar[...]` with "is not valid as type
482
+ # argument". But a *string* ClassVar annotation is legitimate --
483
+ # `from __future__ import annotations` stringizes every
484
+ # annotation, ClassVars included -- and callers (see
485
+ # `_resolve_refs` + `skip_classvar`) still want to see it so they
486
+ # can drop it. Evaluate the forward arg directly and accept it
487
+ # when it is a ClassVar; otherwise re-raise the real error.
488
+ src = (
489
+ original_str
490
+ if original_str is not None
491
+ else typ.__forward_arg__
492
+ )
493
+ evaluated = eval(src, globalns, localns) # noqa: S307
494
+ if _is_class_var(evaluated):
495
+ typ = evaluated
496
+ else:
497
+ raise
498
+ else:
499
+ typ = _eval_type(typ, globalns, localns)
469
500
  if typ in invalid_types:
470
501
  raise InvalidAnnotation(typ)
471
502
  return alias_types.get(typ, typ)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mode-streaming
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: AsyncIO Service-based programming
5
5
  Author-email: Ask Solem Hoel <ask@robinhood.com>
6
6
  Maintainer-email: William Barnhart <williambbarnhart@gmail.com>
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
20
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
21
22
  Classifier: Programming Language :: Python :: Implementation :: PyPy
22
23
  Classifier: Operating System :: POSIX
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.5.1",
2
+ "tag": "0.5.3",
3
3
  "distance": 0,
4
- "node": "gb48d2dd45818a9f01f6e78159e12330e010d1f71",
4
+ "node": "g9a3d6aeae585ec2ef1b322756c53c6f1af9b0bf2",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-07-19"
@@ -40,6 +40,7 @@ classifiers = [
40
40
  "Programming Language :: Python :: 3.11",
41
41
  "Programming Language :: Python :: 3.12",
42
42
  "Programming Language :: Python :: 3.13",
43
+ "Programming Language :: Python :: 3.14",
43
44
  "Programming Language :: Python :: Implementation :: CPython",
44
45
  "Programming Language :: Python :: Implementation :: PyPy",
45
46
  "Operating System :: POSIX",
@@ -0,0 +1,94 @@
1
+ import asyncio
2
+ import threading
3
+ from unittest.mock import patch
4
+
5
+ from mode.utils.loops import get_event_loop
6
+
7
+
8
+ def test_get_event_loop__returns_running_loop_when_running():
9
+ async def main():
10
+ return get_event_loop()
11
+
12
+ running = asyncio.new_event_loop()
13
+ try:
14
+ got = running.run_until_complete(main())
15
+ assert got is running
16
+ finally:
17
+ running.close()
18
+
19
+
20
+ def test_get_event_loop__reuses_current_loop_when_set():
21
+ loop = asyncio.new_event_loop()
22
+ asyncio.set_event_loop(loop)
23
+ try:
24
+ assert get_event_loop() is loop
25
+ finally:
26
+ asyncio.set_event_loop(None)
27
+ loop.close()
28
+
29
+
30
+ def test_get_event_loop__creates_loop_when_none_set():
31
+ # Emulates Python 3.14, where asyncio.get_event_loop() no longer creates
32
+ # a loop implicitly and raises RuntimeError instead.
33
+ asyncio.set_event_loop(None)
34
+ loop = get_event_loop()
35
+ try:
36
+ assert isinstance(loop, asyncio.AbstractEventLoop)
37
+ assert asyncio.get_event_loop() is loop
38
+ finally:
39
+ asyncio.set_event_loop(None)
40
+ loop.close()
41
+
42
+
43
+ def test_get_event_loop__is_idempotent_without_running_loop():
44
+ asyncio.set_event_loop(None)
45
+ first = get_event_loop()
46
+ try:
47
+ assert get_event_loop() is first
48
+ finally:
49
+ asyncio.set_event_loop(None)
50
+ first.close()
51
+
52
+
53
+ def test_get_event_loop__caches_loop_without_repeated_lookup():
54
+ # The whole point of the thread-local cache: once a loop has been
55
+ # resolved for a thread, later calls on that thread must not re-invoke
56
+ # (and potentially re-raise/re-catch RuntimeError from)
57
+ # asyncio.get_event_loop() again.
58
+ asyncio.set_event_loop(None)
59
+ first = get_event_loop()
60
+ try:
61
+ with patch(
62
+ "mode.utils.loops.asyncio.get_event_loop"
63
+ ) as get_event_loop_mock:
64
+ second = get_event_loop()
65
+ assert second is first
66
+ get_event_loop_mock.assert_not_called()
67
+ finally:
68
+ asyncio.set_event_loop(None)
69
+ first.close()
70
+
71
+
72
+ def test_get_event_loop__caches_per_thread_not_globally():
73
+ # Mode runs a dedicated event loop per ServiceThread worker thread
74
+ # (see mode.threads.ServiceThread), so the cache must be thread-local:
75
+ # one thread's cached loop must never leak into another thread's call.
76
+ asyncio.set_event_loop(None)
77
+ main_loop = get_event_loop()
78
+ other_loop_holder: dict = {}
79
+
80
+ def other_thread() -> None:
81
+ other_loop_holder["loop"] = get_event_loop()
82
+
83
+ try:
84
+ thread = threading.Thread(target=other_thread)
85
+ thread.start()
86
+ thread.join()
87
+
88
+ other_loop = other_loop_holder["loop"]
89
+ assert other_loop is not main_loop
90
+ assert get_event_loop() is main_loop
91
+ finally:
92
+ asyncio.set_event_loop(None)
93
+ main_loop.close()
94
+ other_loop_holder["loop"].close()
@@ -275,6 +275,38 @@ def test_annotations__stop_excludes_base_above_stop():
275
275
  assert fields == {"foo": int}
276
276
 
277
277
 
278
+ def test_annotations__preserves_function_local_class_identity():
279
+ # Regression test: a field annotated with a class defined inside a
280
+ # function (``__qualname__`` contains "<locals>") must be returned as the
281
+ # actual class object, not rewritten to its bare name string. Callers
282
+ # such as faust.Record match a field's resolved type by identity against
283
+ # a coercion mapping, which silently breaks if the type is replaced by a
284
+ # string.
285
+ class Local:
286
+ value: int
287
+
288
+ class Holder:
289
+ item: Local
290
+
291
+ fields, _ = annotations(Holder, globalns=globals(), localns=locals())
292
+
293
+ assert fields["item"] is Local
294
+ assert not isinstance(fields["item"], str)
295
+
296
+
297
+ def test_eval_type__string_classvar_is_not_rejected():
298
+ # Regression test: a *string* ClassVar annotation (as produced by
299
+ # `from __future__ import annotations`) must not raise. Passing the
300
+ # forward ref straight to typing._eval_type runs typing._type_check,
301
+ # which rejects ClassVar[...] with "is not valid as type argument".
302
+ from typing import ClassVar as _ClassVar
303
+
304
+ result = eval_type(
305
+ "ClassVar[int]", globalns={"ClassVar": _ClassVar}, localns={}
306
+ )
307
+ assert result == _ClassVar[int]
308
+
309
+
278
310
  @pytest.mark.parametrize(
279
311
  "input,expected",
280
312
  [
@@ -1,48 +0,0 @@
1
- import asyncio
2
-
3
- from mode.utils.loops import get_event_loop
4
-
5
-
6
- def test_get_event_loop__returns_running_loop_when_running():
7
- async def main():
8
- return get_event_loop()
9
-
10
- running = asyncio.new_event_loop()
11
- try:
12
- got = running.run_until_complete(main())
13
- assert got is running
14
- finally:
15
- running.close()
16
-
17
-
18
- def test_get_event_loop__reuses_current_loop_when_set():
19
- loop = asyncio.new_event_loop()
20
- asyncio.set_event_loop(loop)
21
- try:
22
- assert get_event_loop() is loop
23
- finally:
24
- asyncio.set_event_loop(None)
25
- loop.close()
26
-
27
-
28
- def test_get_event_loop__creates_loop_when_none_set():
29
- # Emulates Python 3.14, where asyncio.get_event_loop() no longer creates
30
- # a loop implicitly and raises RuntimeError instead.
31
- asyncio.set_event_loop(None)
32
- loop = get_event_loop()
33
- try:
34
- assert isinstance(loop, asyncio.AbstractEventLoop)
35
- assert asyncio.get_event_loop() is loop
36
- finally:
37
- asyncio.set_event_loop(None)
38
- loop.close()
39
-
40
-
41
- def test_get_event_loop__is_idempotent_without_running_loop():
42
- asyncio.set_event_loop(None)
43
- first = get_event_loop()
44
- try:
45
- assert get_event_loop() is first
46
- finally:
47
- asyncio.set_event_loop(None)
48
- first.close()
File without changes
File without changes
File without changes
File without changes