vortexpy 6.0.1__tar.gz → 6.1.0__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 (154) hide show
  1. {vortexpy-6.0.1 → vortexpy-6.1.0}/PKG-INFO +1 -1
  2. {vortexpy-6.0.1 → vortexpy-6.1.0}/pyproject.toml +1 -1
  3. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/SerialiseUtil.py +8 -0
  4. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/Tuple.py +46 -0
  5. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TupleDocumentConverter.py +3 -4
  6. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TupleTest.py +79 -1
  7. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexFactory.py +112 -128
  8. vortexpy-6.1.0/vortex/VortexServerTcp.py +213 -0
  9. vortexpy-6.1.0/vortex/VortexServerTcp_test.py +121 -0
  10. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexServerWebsocket.py +67 -26
  11. vortexpy-6.1.0/vortex/VortexServerWebsocket_test.py +116 -0
  12. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexSession.py +10 -8
  13. vortexpy-6.1.0/vortex/VortexSession_test.py +57 -0
  14. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/__init__.py +1 -1
  15. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/data_loader/TupleDataLoaderDelegate.py +1 -1
  16. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataActionClient.py +19 -11
  17. vortexpy-6.1.0/vortex/handler/TupleDataActionClient_test.py +102 -0
  18. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataObservableHandler.py +10 -0
  19. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/rpc/RPC.py +1 -0
  20. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortexpy.egg-info/PKG-INFO +1 -1
  21. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortexpy.egg-info/SOURCES.txt +2 -0
  22. vortexpy-6.0.1/vortex/VortexServerTcp.py +0 -133
  23. vortexpy-6.0.1/vortex/VortexServerWebsocket_test.py +0 -22
  24. vortexpy-6.0.1/vortex/VortexSession_test.py +0 -23
  25. {vortexpy-6.0.1 → vortexpy-6.1.0}/LICENSE +0 -0
  26. {vortexpy-6.0.1 → vortexpy-6.1.0}/README.md +0 -0
  27. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/TwistedDeferredTimeout.py +0 -0
  28. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/__init__.py +0 -0
  29. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/run_http_upgraded_websocket_ping_client.py +0 -0
  30. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/run_http_upgraded_websocket_ping_server.py +0 -0
  31. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
  32. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
  33. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/run_websocket_rpc_test_client.py +0 -0
  34. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/run_websocket_rpc_test_server.py +0 -0
  35. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/setup_example_logging.py +0 -0
  36. {vortexpy-6.0.1 → vortexpy-6.1.0}/example/store_and_load_json.py +0 -0
  37. {vortexpy-6.0.1 → vortexpy-6.1.0}/setup.cfg +0 -0
  38. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/DataWrapTuple.py +0 -0
  39. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/DeferUtil.py +0 -0
  40. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/DeferUtil_test.py +0 -0
  41. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/Jsonable.py +0 -0
  42. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
  43. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
  44. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/MtlsServerSslOptions.py +0 -0
  45. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/Payload.py +0 -0
  46. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadEndpoint.py +0 -0
  47. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadEndpointTest.py +0 -0
  48. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadEnvelope.py +0 -0
  49. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadFilterKeys.py +0 -0
  50. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadIO.py +0 -0
  51. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadPriority.py +0 -0
  52. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadResponse.py +0 -0
  53. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadResponseTest.py +0 -0
  54. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadResponse_test.py +0 -0
  55. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PayloadTest.py +0 -0
  56. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/PerformTestActionTuple.py +0 -0
  57. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/SerialiseUtil_test.py +0 -0
  58. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TestTuple.py +0 -0
  59. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TupleAction.py +0 -0
  60. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TupleBson.py +0 -0
  61. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TupleBson_test.py +0 -0
  62. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TupleDataForTest.py +0 -0
  63. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TuplePerfTest.py +0 -0
  64. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  65. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TupleRestful.py +0 -0
  66. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TupleRestful_test.py +0 -0
  67. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/TupleSelector.py +0 -0
  68. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexABC.py +0 -0
  69. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexAccessControlContextABC.py +0 -0
  70. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexAuthenticatorABC.py +0 -0
  71. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexClientHttp.py +0 -0
  72. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexClientHttpTest.py +0 -0
  73. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexClientTcp.py +0 -0
  74. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexClientWebsocketFactory.py +0 -0
  75. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexConnectionABC.py +0 -0
  76. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexEndpointPermission.py +0 -0
  77. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexFactory_test.py +0 -0
  78. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexPayloadProtocol.py +0 -0
  79. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexPermission.py +0 -0
  80. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexPermissionError.py +0 -0
  81. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexServer.py +0 -0
  82. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexServerConnection.py +0 -0
  83. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexServerConnection_test.py +0 -0
  84. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexServerHttpResource.py +0 -0
  85. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexServer_test.py +0 -0
  86. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  87. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  88. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexTest.py +0 -0
  89. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexUserDetailsABC.py +0 -0
  90. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexUtil.py +0 -0
  91. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexWritePushProducer.py +0 -0
  92. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/VortexWritePushProducer_test.py +0 -0
  93. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
  94. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/data_loader/TupleDataLiveLockManager_test.py +0 -0
  95. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/data_loader/TupleDataLoader.py +0 -0
  96. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  97. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  98. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/data_loader/__init__.py +0 -0
  99. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  100. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  101. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  102. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/event_bus/__init__.py +0 -0
  103. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleActionProcessor.py +0 -0
  104. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleActionProcessorProxy.py +0 -0
  105. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  106. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
  107. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleActionProcessor_test.py +0 -0
  108. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleActionPushOffline.py +0 -0
  109. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleActionPushOffline_test.py +0 -0
  110. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  111. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataObservableCache.py +0 -0
  112. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataObservableCache_test.py +0 -0
  113. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataObservableHandler_test.py +0 -0
  114. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataObservableProxyHandler.py +0 -0
  115. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
  116. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  117. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataObserverClient.py +0 -0
  118. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/TupleDataProviderABC.py +0 -0
  119. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  120. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/handler/__init__.py +0 -0
  121. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/mypy_plugin.py +0 -0
  122. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/payload_many_tuples_benchmark_test.py +0 -0
  123. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/py.typed +0 -0
  124. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/restful/GzippedDataHttpClient.py +0 -0
  125. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  126. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  127. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/restful/RestfulHttpClient.py +0 -0
  128. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/restful/RestfulResource.py +0 -0
  129. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/restful/__init__.py +0 -0
  130. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/restful/restful_http_client_test.py +0 -0
  131. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/restful/restful_resource_test.py +0 -0
  132. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/rpc/RPCTest.py +0 -0
  133. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/rpc/RPC_test.py +0 -0
  134. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/rpc/__init__.py +0 -0
  135. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
  136. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  137. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  138. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/sqla_orm/__init__.py +0 -0
  139. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/storage/TupleActionStorageSqlite.py +0 -0
  140. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/storage/TupleStorageSqlite.py +0 -0
  141. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/storage/__init__.py +0 -0
  142. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/tuple_json_schema/__init__.py +0 -0
  143. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/tuple_json_schema/constants.py +0 -0
  144. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  145. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  146. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  147. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  148. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/tuple_test_fixtures.py +0 -0
  149. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/twisted_runtime_metrics.py +0 -0
  150. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortex/twisted_runtime_metrics_test.py +0 -0
  151. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortexpy.egg-info/dependency_links.txt +0 -0
  152. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortexpy.egg-info/entry_points.txt +0 -0
  153. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortexpy.egg-info/requires.txt +0 -0
  154. {vortexpy-6.0.1 → vortexpy-6.1.0}/vortexpy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 6.0.1
3
+ Version: 6.1.0
4
4
  Summary: Flying Ion's observable, routable, data serialisation and transport code.
5
5
  Author-email: Flying Ion Pty Ltd <contact@flyingion.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortexpy"
7
- version = "6.0.1"
7
+ version = "6.1.0"
8
8
  license = {text = "MIT"}
9
9
  authors = [{name = "Flying Ion Pty Ltd", email = "contact@flyingion.com"}]
10
10
  description = "Flying Ion's observable, routable, data serialisation and transport code."
@@ -290,10 +290,18 @@ def fromRapuiType(typeName: str):
290
290
  def utilTypeHintsForClass(cls: type) -> dict[str, typing.Any]:
291
291
  """Resolved type hints for a class, cached per class.
292
292
 
293
+ Tuple classes resolve through ``Tuple.typeHints()`` — the canonical
294
+ per-class store; other classes are cached by this function's
295
+ ``lru_cache``.
296
+
293
297
  Returns an empty dict when hints can not be resolved (for example
294
298
  unresolvable forward references) — enum coercion degrades to no-op.
295
299
  """
300
+ from vortex.Tuple import Tuple
301
+
296
302
  try:
303
+ if issubclass(cls, Tuple):
304
+ return cls.typeHints()
297
305
  return typing.get_type_hints(cls)
298
306
  except Exception:
299
307
  return {}
@@ -17,6 +17,7 @@ from datetime import datetime
17
17
  from typing import Dict, Optional, Any
18
18
  from typing import List
19
19
  from typing import Type
20
+ from typing import get_type_hints
20
21
 
21
22
  from sqlalchemy.orm.attributes import InstrumentedAttribute
22
23
  from sqlalchemy.orm.properties import RelationshipProperty
@@ -648,6 +649,51 @@ class Tuple(Jsonable, _TupleToPlainJsonMixin, _TupleToSqlaJsonMixin):
648
649
  def tupleFieldNames(cls) -> List[str]:
649
650
  return cls.__fieldNames__
650
651
 
652
+ @classmethod
653
+ def typeHints(cls) -> Dict[str, Any]:
654
+ """Type Hints
655
+
656
+ Resolved type hints for this class, computed once and stored on the
657
+ class itself.
658
+
659
+ Resolution is lazy: tuples reference each other with forward
660
+ references, so hints can only be resolved after all imports
661
+ complete. Failures are not stored — if the hints are unresolvable
662
+ the exception propagates and the next call retries.
663
+
664
+ The hints are stored in the class's own ``__dict__`` so a subclass
665
+ always resolves and stores its own hints, never a base class's.
666
+
667
+ :return: The resolved type hints for this class.
668
+ """
669
+ cachedHints = cls.__dict__.get("__cachedTypeHints__")
670
+ if cachedHints is not None:
671
+ return cachedHints
672
+
673
+ hints = get_type_hints(cls)
674
+ cls.__cachedTypeHints__ = hints
675
+ return hints
676
+
677
+ @classmethod
678
+ def typeHintsWithExtras(cls) -> Dict[str, Any]:
679
+ """Type Hints With Extras
680
+
681
+ Resolved type hints for this class, including ``Annotated`` extras,
682
+ computed once and stored on the class itself.
683
+
684
+ Resolution is lazy and failures are not stored, same as
685
+ :meth:`typeHints`.
686
+
687
+ :return: The resolved type hints for this class, with extras.
688
+ """
689
+ cachedHints = cls.__dict__.get("__cachedTypeHintsWithExtras__")
690
+ if cachedHints is not None:
691
+ return cachedHints
692
+
693
+ hints = get_type_hints(cls, include_extras=True)
694
+ cls.__cachedTypeHintsWithExtras__ = hints
695
+ return hints
696
+
651
697
  @classmethod
652
698
  def tupleJsonSchema(cls) -> "JsonSchemaNodeTuple":
653
699
  """Tuple JSON Schema
@@ -20,7 +20,6 @@ from typing import TYPE_CHECKING, Annotated, Any, NamedTuple, Optional, Type
20
20
 
21
21
  from typing import Union, get_args
22
22
  from typing import get_origin
23
- from typing import get_type_hints
24
23
 
25
24
  from .Tuple import BsonDatetimeFormatE
26
25
  from .Tuple import TUPLE_TYPES_BY_NAME
@@ -751,7 +750,7 @@ class TupleDocumentConverterBase:
751
750
  ) -> TupleValidationResult:
752
751
  errors: list[TupleValidationError] = []
753
752
 
754
- if not get_type_hints(TupleClass):
753
+ if not TupleClass.typeHints():
755
754
  errors.append(
756
755
  cls._makeError(_ROOT_PATH, f"{TupleClass} has no annotations")
757
756
  )
@@ -759,8 +758,8 @@ class TupleDocumentConverterBase:
759
758
 
760
759
  TupleClass = cls._resolveConcreteClass(TupleClass, jsonDict, errors)
761
760
 
762
- typeHints = get_type_hints(TupleClass)
763
- extraTypeHits = get_type_hints(TupleClass, include_extras=True)
761
+ typeHints = TupleClass.typeHints()
762
+ extraTypeHits = TupleClass.typeHintsWithExtras()
764
763
 
765
764
  newTuple = TupleClass()
766
765
 
@@ -9,10 +9,13 @@
9
9
  import datetime
10
10
  import json
11
11
 
12
+ from typing import Annotated, get_type_hints
13
+ from unittest import mock
14
+
12
15
  from twisted.trial import unittest
13
16
 
14
17
  from vortex.TestTuple import TestSubTuple, TestTuple
15
- from vortex.Tuple import TupleHash
18
+ from vortex.Tuple import Tuple, TupleField, TupleHash
16
19
 
17
20
 
18
21
  def makeTupleA():
@@ -124,3 +127,78 @@ class TuplePyTest(unittest.TestCase):
124
127
  self.assertFalse(
125
128
  hashA1 in {hashB1: None}, "TupleHash in fail, A1 in {B1:None}"
126
129
  )
130
+
131
+
132
+ class TupleTypeHintsTest(unittest.TestCase):
133
+ """Tests for the lazy per-class type hint storage on ``Tuple``."""
134
+
135
+ def testTypeHintsCachedOnClass(self):
136
+ """Two calls return the same dict; get_type_hints runs once."""
137
+
138
+ class CachedHintsTuple(Tuple):
139
+ aInt: int = TupleField()
140
+ aStr: str = TupleField()
141
+
142
+ countingHints = mock.Mock(side_effect=get_type_hints)
143
+ with mock.patch("vortex.Tuple.get_type_hints", countingHints):
144
+ hints1 = CachedHintsTuple.typeHints()
145
+ hints2 = CachedHintsTuple.typeHints()
146
+
147
+ self.assertIs(hints1, hints2)
148
+ self.assertEqual(countingHints.call_count, 1)
149
+ self.assertIs(hints1["aInt"], int)
150
+ self.assertIs(hints1["aStr"], str)
151
+
152
+ def testTypeHintsWithExtrasCachedOnClass(self):
153
+ """Annotated extras are kept; get_type_hints runs once."""
154
+
155
+ class CachedExtrasTuple(Tuple):
156
+ aInt: Annotated[int, "marker"] = TupleField()
157
+
158
+ countingHints = mock.Mock(side_effect=get_type_hints)
159
+ with mock.patch("vortex.Tuple.get_type_hints", countingHints):
160
+ hints1 = CachedExtrasTuple.typeHintsWithExtras()
161
+ hints2 = CachedExtrasTuple.typeHintsWithExtras()
162
+
163
+ self.assertIs(hints1, hints2)
164
+ self.assertEqual(countingHints.call_count, 1)
165
+ self.assertEqual(
166
+ countingHints.call_args, mock.call(CachedExtrasTuple, include_extras=True)
167
+ )
168
+ self.assertEqual(hints1["aInt"].__metadata__, ("marker",))
169
+
170
+ def testTypeHintsSubclassStoresOwnHints(self):
171
+ """A subclass never returns a base class's cached hints."""
172
+
173
+ class ParentHintsTuple(Tuple):
174
+ aStr: str = TupleField()
175
+
176
+ class ChildHintsTuple(ParentHintsTuple):
177
+ aInt: int = TupleField()
178
+
179
+ parentHints = ParentHintsTuple.typeHints()
180
+ childHints = ChildHintsTuple.typeHints()
181
+
182
+ self.assertIsNot(parentHints, childHints)
183
+ self.assertIs(parentHints["aStr"], str)
184
+ self.assertNotIn("aInt", parentHints)
185
+ self.assertIs(childHints["aStr"], str)
186
+ self.assertIs(childHints["aInt"], int)
187
+
188
+ def testTypeHintsFailureNotCached(self):
189
+ """A failed resolution propagates and the next call retries."""
190
+
191
+ class FailingHintsTuple(Tuple):
192
+ aInt: int = TupleField()
193
+
194
+ flakyHints = mock.Mock(
195
+ side_effect=[NameError("unresolvable"), {"aInt": int}]
196
+ )
197
+ with mock.patch("vortex.Tuple.get_type_hints", flakyHints):
198
+ self.assertRaises(NameError, FailingHintsTuple.typeHints)
199
+ hints = FailingHintsTuple.typeHints()
200
+ again = FailingHintsTuple.typeHints()
201
+
202
+ self.assertEqual(hints, {"aInt": int})
203
+ self.assertIs(hints, again)
204
+ self.assertEqual(flakyHints.call_count, 2)