mode-streaming 0.5.0__tar.gz → 0.5.2__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 (163) hide show
  1. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/PKG-INFO +1 -1
  2. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/objects.py +98 -42
  3. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode_streaming.egg-info/PKG-INFO +1 -1
  4. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode_streaming.egg-info/scm_version.json +2 -2
  5. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/utils/test_objects.py +56 -0
  6. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/.editorconfig +0 -0
  7. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/.github/ISSUE_TEMPLATE.md +0 -0
  8. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  9. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/.github/workflows/deploy-docs.yml +0 -0
  10. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/.github/workflows/publish.yml +0 -0
  11. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/.github/workflows/tests.yml +0 -0
  12. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/.gitignore +0 -0
  13. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/.pre-commit-config.yaml +0 -0
  14. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/AUTHORS +0 -0
  15. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/CHANGELOG.md +0 -0
  16. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/CODE_OF_CONDUCT.md +0 -0
  17. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/CONTRIBUTING.md +0 -0
  18. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/LICENSE +0 -0
  19. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/README.md +0 -0
  20. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/changelog.md +0 -0
  21. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/contributing.md +0 -0
  22. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/creating-service.md +0 -0
  23. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/example-service.md +0 -0
  24. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/example-webapp.md +0 -0
  25. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/index.md +0 -0
  26. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.debug.md +0 -0
  27. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.exceptions.md +0 -0
  28. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.locals.md +0 -0
  29. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.loop.eventlet.md +0 -0
  30. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.loop.gevent.md +0 -0
  31. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.loop.md +0 -0
  32. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.loop.uvloop.md +0 -0
  33. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.proxy.md +0 -0
  34. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.services.md +0 -0
  35. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.signals.md +0 -0
  36. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.supervisors.md +0 -0
  37. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.threads.md +0 -0
  38. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.timers.md +0 -0
  39. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.types.md +0 -0
  40. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.types.services.md +0 -0
  41. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.types.signals.md +0 -0
  42. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.types.supervisors.md +0 -0
  43. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.aiter.md +0 -0
  44. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.collections.md +0 -0
  45. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.compat.md +0 -0
  46. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.contexts.md +0 -0
  47. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.cron.md +0 -0
  48. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.futures.md +0 -0
  49. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.graphs.md +0 -0
  50. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.imports.md +0 -0
  51. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.locals.md +0 -0
  52. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.locks.md +0 -0
  53. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.logging.md +0 -0
  54. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.loops.md +0 -0
  55. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.mocks.md +0 -0
  56. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.objects.md +0 -0
  57. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.queues.md +0 -0
  58. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.text.md +0 -0
  59. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.times.md +0 -0
  60. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.tracebacks.md +0 -0
  61. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.trees.md +0 -0
  62. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.types.graphs.md +0 -0
  63. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.utils.types.trees.md +0 -0
  64. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/docs/references/mode.worker.md +0 -0
  65. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/examples/service.py +0 -0
  66. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/examples/webapp.py +0 -0
  67. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mkdocs.yml +0 -0
  68. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/__init__.py +0 -0
  69. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/debug.py +0 -0
  70. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/exceptions.py +0 -0
  71. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/locals.py +0 -0
  72. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/loop/__init__.py +0 -0
  73. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/loop/_gevent_loop.py +0 -0
  74. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/loop/eventlet.py +0 -0
  75. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/loop/gevent.py +0 -0
  76. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/loop/uvloop.py +0 -0
  77. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/proxy.py +0 -0
  78. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/py.typed +0 -0
  79. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/services.py +0 -0
  80. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/signals.py +0 -0
  81. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/supervisors.py +0 -0
  82. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/threads.py +0 -0
  83. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/timers.py +0 -0
  84. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/types/__init__.py +0 -0
  85. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/types/services.py +0 -0
  86. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/types/signals.py +0 -0
  87. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/types/supervisors.py +0 -0
  88. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/__init__.py +0 -0
  89. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/aiter.py +0 -0
  90. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/collections.py +0 -0
  91. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/compat.py +0 -0
  92. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/contexts.py +0 -0
  93. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/cron.py +0 -0
  94. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/futures.py +0 -0
  95. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/graphs/__init__.py +0 -0
  96. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/graphs/formatter.py +0 -0
  97. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/graphs/graph.py +0 -0
  98. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/imports.py +0 -0
  99. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/locals.py +0 -0
  100. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/locks.py +0 -0
  101. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/logging.py +0 -0
  102. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/loops.py +0 -0
  103. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/mocks.py +0 -0
  104. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/queues.py +0 -0
  105. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/text.py +0 -0
  106. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/times.py +0 -0
  107. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/tracebacks.py +0 -0
  108. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/trees.py +0 -0
  109. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/types/__init__.py +0 -0
  110. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/types/graphs.py +0 -0
  111. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/utils/types/trees.py +0 -0
  112. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode/worker.py +0 -0
  113. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode_streaming.egg-info/SOURCES.txt +0 -0
  114. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode_streaming.egg-info/dependency_links.txt +0 -0
  115. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode_streaming.egg-info/requires.txt +0 -0
  116. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode_streaming.egg-info/scm_file_list.json +0 -0
  117. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/mode_streaming.egg-info/top_level.txt +0 -0
  118. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/pyproject.toml +0 -0
  119. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/requirements-docs.txt +0 -0
  120. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/requirements-tests.txt +0 -0
  121. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/requirements.txt +0 -0
  122. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/scripts/build-docs.sh +0 -0
  123. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/scripts/build.sh +0 -0
  124. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/scripts/bump.sh +0 -0
  125. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/scripts/clean.sh +0 -0
  126. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/scripts/format.sh +0 -0
  127. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/scripts/lint.sh +0 -0
  128. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/scripts/tests.sh +0 -0
  129. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/setup.cfg +0 -0
  130. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/__init__.py +0 -0
  131. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/conftest.py +0 -0
  132. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/__init__.py +0 -0
  133. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/test_mode.py +0 -0
  134. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/test_proxy.py +0 -0
  135. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/test_service.py +0 -0
  136. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/test_signals.py +0 -0
  137. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/test_supervisors.py +0 -0
  138. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/test_timers.py +0 -0
  139. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/__init__.py +0 -0
  140. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_aiter.py +0 -0
  141. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_collections.py +0 -0
  142. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_compat.py +0 -0
  143. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_contexts.py +0 -0
  144. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_futures.py +0 -0
  145. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_locals.py +0 -0
  146. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_queues.py +0 -0
  147. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_text.py +0 -0
  148. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_times.py +0 -0
  149. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_tracebacks.py +0 -0
  150. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/functional/utils/test_trees.py +0 -0
  151. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/__init__.py +0 -0
  152. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/test_debug.py +0 -0
  153. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/test_locals.py +0 -0
  154. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/test_services.py +0 -0
  155. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/test_supervisors.py +0 -0
  156. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/test_threads.py +0 -0
  157. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/test_worker.py +0 -0
  158. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/utils/__init__.py +0 -0
  159. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/utils/test_cron.py +0 -0
  160. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/utils/test_imports.py +0 -0
  161. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/utils/test_locks.py +0 -0
  162. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/utils/test_logging.py +0 -0
  163. {mode_streaming-0.5.0 → mode_streaming-0.5.2}/tests/unit/utils/test_loops.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mode-streaming
3
- Version: 0.5.0
3
+ Version: 0.5.2
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>
@@ -28,7 +28,6 @@ from typing import (
28
28
  cast,
29
29
  get_args,
30
30
  get_origin,
31
- get_type_hints,
32
31
  )
33
32
 
34
33
  try:
@@ -39,20 +38,39 @@ except ImportError:
39
38
  return t
40
39
 
41
40
 
42
- try:
43
- from typing import _type_check # type: ignore
44
- except ImportError:
45
-
46
- def _type_check(arg, msg, is_argument=True, module=None): # type: ignore
47
- return arg
48
-
49
-
50
41
  def _is_class_var(typ):
51
42
  # Works for typing.ClassVar and types.GenericAlias (Python 3.9+)
52
43
  origin = getattr(typ, "__origin__", None)
53
44
  return origin is ClassVar
54
45
 
55
46
 
47
+ if sys.version_info >= (3, 10):
48
+ from inspect import get_annotations as _own_annotations
49
+ else:
50
+
51
+ def _own_annotations(
52
+ cls: type,
53
+ *,
54
+ globals: Optional[dict] = None,
55
+ locals: Optional[dict] = None,
56
+ eval_str: bool = False,
57
+ ) -> dict:
58
+ """Backport of :func:`inspect.get_annotations` for Python 3.9.
59
+
60
+ Returns only the annotations declared directly in ``cls.__dict__``,
61
+ never inherited ones -- matching the 3.10+ stdlib function this
62
+ shadows, which is what makes it safe to use per-class inside a
63
+ bounded MRO walk (see ``local_annotations`` below).
64
+ """
65
+ ann = cls.__dict__.get("__annotations__", {})
66
+ if eval_str:
67
+ ann = {
68
+ k: (eval(v, globals, locals) if isinstance(v, str) else v) # noqa: S307
69
+ for k, v in ann.items()
70
+ }
71
+ return dict(ann)
72
+
73
+
56
74
  def _get_globalns(cls):
57
75
  # Get the global namespace for a class
58
76
  module = sys.modules.get(cls.__module__)
@@ -348,11 +366,18 @@ def annotations(
348
366
  )
349
367
  )
350
368
 
351
- # Normalize all field types for forward refs
352
- normalized_fields = {
353
- k: _normalize_forwardref(v) for k, v in fields.items()
354
- }
355
- 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
356
381
 
357
382
 
358
383
  def local_annotations(
@@ -364,9 +389,31 @@ def local_annotations(
364
389
  globalns: Optional[dict[str, Any]] = None,
365
390
  localns: Optional[dict[str, Any]] = None,
366
391
  ) -> Iterable[tuple[str, type]]:
367
- d = get_type_hints(
368
- cls, globalns if globalns is not None else _get_globalns(cls), localns
369
- )
392
+ # NOTE: Must only consider annotations defined directly on `cls`, not
393
+ # inherited ones. `annotations()` (the caller) already walks the MRO
394
+ # itself via `iter_mro_reversed(cls, stop=stop)` and calls
395
+ # `local_annotations` once per class in that bounded range, so callers
396
+ # can exclude base classes at/above `stop` (e.g. faust.Record excludes
397
+ # its internal ModelT/Model bases this way). Using
398
+ # `typing.get_type_hints(cls, ...)` here defeats that: it always merges
399
+ # annotations from the *entire* real MRO regardless of `stop`, so a
400
+ # non-ClassVar annotation on an excluded base (e.g.
401
+ # `ModelT.__evaluated_fields__`) would leak back in as if it were a
402
+ # field of every subclass.
403
+ #
404
+ # Plain `cls.__annotations__` attribute access is *not* safe for this
405
+ # either: on Python < 3.10 it falls back to an inherited base's
406
+ # `__annotations__` via normal MRO lookup when `cls` itself has none of
407
+ # its own (re-introducing the same kind of leak), and on Python 3.14+
408
+ # (PEP 649, deferred evaluation of annotations) direct `__annotations__`
409
+ # access on a class can behave unreliably -- the stdlib explicitly
410
+ # recommends `inspect.get_annotations()` instead, which reads only
411
+ # `cls.__dict__["__annotations__"]` (own annotations, no MRO fallback)
412
+ # and is written to handle PEP 649 correctly. `_own_annotations` here is
413
+ # that function, with a same-behavior backport for 3.9. String/ForwardRef
414
+ # annotations are still resolved below, per value, via
415
+ # `_resolve_refs`/`eval_type`.
416
+ d = _own_annotations(cls)
370
417
  return _resolve_refs(
371
418
  d,
372
419
  globalns if globalns is not None else _get_globalns(cls),
@@ -413,39 +460,48 @@ def eval_type(
413
460
  """
414
461
  invalid_types = invalid_types or set()
415
462
  alias_types = alias_types or {}
463
+ original_str = typ if isinstance(typ, str) else None
416
464
  if isinstance(typ, str):
417
465
  typ = ForwardRef(typ)
466
+ # `typing._eval_type` (imported above as `_eval_type`) already resolves
467
+ # ForwardRef instances directly -- it is the same stdlib function
468
+ # `typing.get_type_hints()` itself relies on, so it always matches
469
+ # whatever internal ForwardRef implementation the running interpreter
470
+ # has (this previously went through a hand-rolled evaluator here that
471
+ # poked at ForwardRef's private __forward_evaluated__/__forward_code__/
472
+ # __forward_value__ attributes as a workaround for Python 3.6/3.7; those
473
+ # attributes are not part of any stable API and are no longer present at
474
+ # all on Python 3.14's ForwardRef, which raised AttributeError. mode's
475
+ # floor is Python 3.9, well past the versions that workaround targeted).
418
476
  if isinstance(typ, ForwardRef):
419
- typ = _ForwardRef_safe_eval(typ, globalns, localns)
420
- typ = _eval_type(typ, globalns, localns)
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)
421
500
  if typ in invalid_types:
422
501
  raise InvalidAnnotation(typ)
423
502
  return alias_types.get(typ, typ)
424
503
 
425
504
 
426
- def _ForwardRef_safe_eval(
427
- ref: ForwardRef,
428
- globalns: Optional[dict[str, Any]] = None,
429
- localns: Optional[dict[str, Any]] = None,
430
- ) -> type:
431
- # On 3.6/3.7 ForwardRef._evaluate crashes if str references ClassVar
432
- if not ref.__forward_evaluated__:
433
- if globalns is None and localns is None:
434
- globalns = localns = {}
435
- elif globalns is None:
436
- globalns = localns
437
- elif localns is None:
438
- localns = globalns
439
- val = eval(ref.__forward_code__, globalns, localns) # noqa: S307
440
- if not _is_class_var(val):
441
- val = _type_check(
442
- val, "Forward references must evaluate to types."
443
- )
444
- ref.__forward_value__ = val
445
- ref.__forward_evaluated__ = True
446
- return ref.__forward_value__
447
-
448
-
449
505
  def iter_mro_reversed(cls: type, stop: type) -> Iterable[type]:
450
506
  """Iterate over superclasses, in reverse Method Resolution Order.
451
507
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mode-streaming
3
- Version: 0.5.0
3
+ Version: 0.5.2
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>
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.5.0",
2
+ "tag": "0.5.2",
3
3
  "distance": 0,
4
- "node": "gea836e7025a4ac52dcf644871e1b20c3c43710f9",
4
+ "node": "g95118563cf82e6445e255bcf2fa7beeb1b23b5d3",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-07-19"
@@ -251,6 +251,62 @@ def test_annotations__no_local_ns_raises():
251
251
  annotations(X, globalns=None, localns=None)
252
252
 
253
253
 
254
+ def test_annotations__stop_excludes_base_above_stop():
255
+ # Regression test: a non-ClassVar annotation on a base class *above*
256
+ # `stop` must not leak into the fields of a subclass, even though
257
+ # Python's typing.get_type_hints() (which local_annotations() must NOT
258
+ # use directly) would normally merge annotations across the whole real
259
+ # MRO regardless of any `stop` boundary.
260
+ class Base:
261
+ # Deliberately not ClassVar, mirroring faust ModelT.__evaluated_fields__.
262
+ internal: int = 0
263
+
264
+ class Middle(Base):
265
+ pass
266
+
267
+ class Leaf(Middle):
268
+ foo: int
269
+
270
+ fields, _ = annotations(
271
+ Leaf, stop=Middle, globalns=globals(), localns=locals()
272
+ )
273
+
274
+ assert "internal" not in fields
275
+ assert fields == {"foo": int}
276
+
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
+
254
310
  @pytest.mark.parametrize(
255
311
  "input,expected",
256
312
  [
File without changes
File without changes
File without changes
File without changes