iris-pex-embedded-python 4.0.0b5__tar.gz → 4.0.0b7__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 (103) hide show
  1. {iris_pex_embedded_python-4.0.0b5/src/iris_pex_embedded_python.egg-info → iris_pex_embedded_python-4.0.0b7}/PKG-INFO +2 -2
  2. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/README.md +2 -2
  3. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/pyproject.toml +1 -1
  4. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/__init__.py +2 -0
  5. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Common.cls +11 -2
  6. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
  7. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Utils.cls +7 -0
  8. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/business_operation.py +6 -5
  9. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/business_process.py +6 -5
  10. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/business_service.py +6 -5
  11. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/common.py +65 -15
  12. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/messages/decorators.py +1 -0
  13. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/messages/dispatch.py +151 -16
  14. iris_pex_embedded_python-4.0.0b7/src/iop/migration/manifest.py +534 -0
  15. iris_pex_embedded_python-4.0.0b7/src/iop/migration/plans.py +80 -0
  16. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/migration/utils.py +143 -227
  17. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/component.py +1 -7
  18. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/declarations.py +34 -14
  19. iris_pex_embedded_python-4.0.0b7/src/iop/production/declarative.py +151 -0
  20. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/diff.py +0 -1
  21. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/import_.py +1 -0
  22. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/model.py +77 -150
  23. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/reconstruction.py +1 -2
  24. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/rendering.py +66 -64
  25. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/types.py +4 -9
  26. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7/src/iris_pex_embedded_python.egg-info}/PKG-INFO +2 -2
  27. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iris_pex_embedded_python.egg-info/SOURCES.txt +2 -0
  28. iris_pex_embedded_python-4.0.0b5/src/iop/migration/plans.py +0 -182
  29. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/LICENSE +0 -0
  30. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/setup.cfg +0 -0
  31. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/__main__.py +0 -0
  32. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cli/__init__.py +0 -0
  33. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cli/formatting.py +0 -0
  34. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cli/main.py +0 -0
  35. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cli/parser.py +0 -0
  36. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cli/types.py +0 -0
  37. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/BusinessOperation.cls +0 -0
  38. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/BusinessProcess.cls +0 -0
  39. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/BusinessService.cls +0 -0
  40. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Director.cls +0 -0
  41. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Duplex/Operation.cls +0 -0
  42. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Duplex/Process.cls +0 -0
  43. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Duplex/Service.cls +0 -0
  44. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Generator/Message/Ack.cls +0 -0
  45. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Generator/Message/Poll.cls +0 -0
  46. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Generator/Message/Start.cls +0 -0
  47. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Generator/Message/StartPickle.cls +0 -0
  48. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Generator/Message/Stop.cls +0 -0
  49. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/InboundAdapter.cls +0 -0
  50. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Message/JSONSchema.cls +0 -0
  51. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Message.cls +0 -0
  52. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/OutboundAdapter.cls +0 -0
  53. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/PickleMessage.cls +0 -0
  54. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/PrivateSession/Message/Ack.cls +0 -0
  55. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/PrivateSession/Message/Poll.cls +0 -0
  56. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/PrivateSession/Message/Start.cls +0 -0
  57. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/PrivateSession/Message/Stop.cls +0 -0
  58. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Projection.cls +0 -0
  59. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Service/Remote/Handler.cls +0 -0
  60. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Service/Remote/Rest/v1.cls +0 -0
  61. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Test.cls +0 -0
  62. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/cls/IOP/Wrapper.cls +0 -0
  63. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/__init__.py +0 -0
  64. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/async_request.py +0 -0
  65. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/business_host.py +0 -0
  66. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/debugpy.py +0 -0
  67. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/generator_request.py +0 -0
  68. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/inbound_adapter.py +0 -0
  69. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/log_manager.py +0 -0
  70. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/outbound_adapter.py +0 -0
  71. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/polling_business_service.py +0 -0
  72. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/private_session_duplex.py +0 -0
  73. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/private_session_process.py +0 -0
  74. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/components/settings.py +0 -0
  75. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/messages/__init__.py +0 -0
  76. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/messages/base.py +0 -0
  77. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/messages/persistent.py +0 -0
  78. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/messages/serialization.py +0 -0
  79. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/messages/validation.py +0 -0
  80. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/migration/__init__.py +0 -0
  81. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/migration/io.py +0 -0
  82. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/__init__.py +0 -0
  83. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/actions.py +0 -0
  84. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/common.py +0 -0
  85. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/inspection.py +0 -0
  86. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/runtime.py +0 -0
  87. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/production/validation.py +0 -0
  88. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/__init__.py +0 -0
  89. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/director.py +0 -0
  90. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/environment.py +0 -0
  91. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/iris.py +0 -0
  92. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/local.py +0 -0
  93. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/protocol.py +0 -0
  94. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/remote/__init__.py +0 -0
  95. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/remote/client.py +0 -0
  96. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/remote/director.py +0 -0
  97. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/remote/migration.py +0 -0
  98. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/remote/settings.py +0 -0
  99. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iop/runtime/remote/setup.py +0 -0
  100. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iris_pex_embedded_python.egg-info/dependency_links.txt +0 -0
  101. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iris_pex_embedded_python.egg-info/entry_points.txt +0 -0
  102. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iris_pex_embedded_python.egg-info/requires.txt +0 -0
  103. {iris_pex_embedded_python-4.0.0b5 → iris_pex_embedded_python-4.0.0b7}/src/iris_pex_embedded_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iris_pex_embedded_python
3
- Version: 4.0.0b5
3
+ Version: 4.0.0b7
4
4
  Summary: Iris Interoperability based on Embedded Python
5
5
  Author-email: grongier <guillaume.rongier@intersystems.com>
6
6
  License: MIT License
@@ -86,6 +86,6 @@ pip install iris-pex-embedded-python
86
86
 
87
87
  ## Getting Started
88
88
 
89
- If you're new to this project, begin by reading the [installation guide](https://grongierisc.github.io/interoperability-embedded-python/getting-started/installation). Then, follow the [first steps](https://grongierisc.github.io/interoperability-embedded-python/getting-started/first-steps) to create your first Business Operation.
89
+ If you're new to this project, begin by reading the [installation guide](https://grongierisc.github.io/interoperability-embedded-python/getting-started/installation). Then, follow the [first steps](https://grongierisc.github.io/interoperability-embedded-python/getting-started/first-steps) to create your first Python-authored production.
90
90
 
91
91
  Happy coding!
@@ -32,6 +32,6 @@ pip install iris-pex-embedded-python
32
32
 
33
33
  ## Getting Started
34
34
 
35
- If you're new to this project, begin by reading the [installation guide](https://grongierisc.github.io/interoperability-embedded-python/getting-started/installation). Then, follow the [first steps](https://grongierisc.github.io/interoperability-embedded-python/getting-started/first-steps) to create your first Business Operation.
35
+ If you're new to this project, begin by reading the [installation guide](https://grongierisc.github.io/interoperability-embedded-python/getting-started/installation). Then, follow the [first steps](https://grongierisc.github.io/interoperability-embedded-python/getting-started/first-steps) to create your first Python-authored production.
36
36
 
37
- Happy coding!
37
+ Happy coding!
@@ -3,7 +3,7 @@ requires = ["setuptools", "wheel"]
3
3
 
4
4
  [project]
5
5
  name = "iris_pex_embedded_python"
6
- version = "4.0.0b5"
6
+ version = "4.0.0b7"
7
7
  description = "Iris Interoperability based on Embedded Python"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.10"
@@ -16,6 +16,7 @@ from iop.messages.base import (
16
16
  _PydanticMessage,
17
17
  _PydanticPickleMessage,
18
18
  )
19
+ from iop.messages.decorators import handler as handler
19
20
  from iop.messages.persistent import Field as Field
20
21
  from iop.messages.persistent import Model as Model
21
22
  from iop.messages.persistent import _PersistentMessage
@@ -85,6 +86,7 @@ __all__ = [
85
86
  "Utils",
86
87
  "bind_component",
87
88
  "controls",
89
+ "handler",
88
90
  "list_bindings",
89
91
  "register_component",
90
92
  "setting",
@@ -187,7 +187,7 @@ Method OnTearDown() As %Status
187
187
  set tSC = $$$OK
188
188
  if $isObject(..%class) {
189
189
  try {
190
- do ..%class."_dispatch_on_tear_down"()
190
+ do ..%class."_dispatch_on_tear_down"($this)
191
191
  } catch ex {
192
192
  set tSC = ex.AsStatus()
193
193
  }
@@ -313,7 +313,7 @@ ClassMethod OnGetConnections(
313
313
  do pItem.GetModifiedSetting("%classname", .tClassname)
314
314
  do pItem.GetModifiedSetting("%module", .tModule)
315
315
 
316
- // try to instantiate class
316
+ // Prepare the Python import path for connection discovery.
317
317
  if tClasspaths '="" {
318
318
  set sys = ##class(%SYS.Python).Import("sys")
319
319
  set delimiter = $s($system.Version.GetOS()="Windows":";",1:":")
@@ -329,7 +329,14 @@ ClassMethod OnGetConnections(
329
329
  set builtins = ##class(%SYS.Python).Import("builtins")
330
330
  set module = ##class(%SYS.Python).Import(tModule)
331
331
  set class = builtins.getattr(module, tClassname)
332
+ // Allocate without calling Python __init__. Connection discovery must not
333
+ // run user startup code; IoP uses on_init() as the lifecycle hook.
332
334
  set tClass = class."__new__"(class)
335
+ Try {
336
+ do tClass."_warn_if_custom_init"()
337
+ }
338
+ Catch ex {
339
+ }
333
340
 
334
341
  set tPythonList = tClass."on_get_connections"()
335
342
  set tPythonListLen = tPythonList."__len__"()
@@ -535,6 +542,8 @@ Method CreateClassInstance(module As %SYS.Python) As %SYS.Python [ Private ]
535
542
  {
536
543
  set builtins = ##class(%SYS.Python).Import("builtins")
537
544
  set class = builtins.getattr(module, ..%classname)
545
+ // Intentionally bypass Python __init__. IRIS applies handles/settings and
546
+ // then calls on_init() through _dispatch_on_init().
538
547
  quit class."__new__"(class)
539
548
  }
540
549
 
@@ -250,7 +250,7 @@ Method OnTearDown() As %Status
250
250
  #dim tSC As %Status = $$$OK
251
251
  try {
252
252
  If ..%InPrivateSession Set tSC=..StopPrivateSession() Quit:$$$ISERR(tSC)
253
- do ..%class."_dispatch_on_tear_down"()
253
+ do ..%class."_dispatch_on_tear_down"($this)
254
254
  } catch {
255
255
  Set tSC=$$$EnsSystemError
256
256
  }
@@ -281,7 +281,14 @@ ClassMethod GetRemoteClassInfo(
281
281
 
282
282
  // Get the class
283
283
  set class = builtins.getattr(module, pRemoteClassname)
284
+ // Allocate without calling Python __init__. Registration only reads
285
+ // metadata; runtime startup belongs in on_init().
284
286
  set tClass = class."__new__"(class)
287
+ Try {
288
+ Do tClass."_warn_if_custom_init"()
289
+ }
290
+ Catch ex {
291
+ }
285
292
 
286
293
  If $IsObject(tClass) {
287
294
  #; List of information about the class as a whole - $lb(SuperClass, Description, InfoURL, IconURL, Adapter)
@@ -55,11 +55,12 @@ class _BusinessOperation(_BusinessHost):
55
55
  self.Adapter = self.adapter = handle_partner
56
56
  return
57
57
 
58
- def _dispatch_on_init(self, host_object: Any) -> None:
59
- """For internal use only."""
60
- create_dispatch(self)
61
- self.on_init()
62
- return
58
+ def _dispatch_on_init(self, host_object: Any) -> None:
59
+ """For internal use only."""
60
+ self._log_custom_init_warning()
61
+ create_dispatch(self)
62
+ self.on_init()
63
+ return
63
64
 
64
65
  @input_deserializer
65
66
  @output_serializer
@@ -165,11 +165,12 @@ class _BusinessProcess(_BusinessHost):
165
165
  self._save_persistent_properties(host_object)
166
166
  return
167
167
 
168
- def _dispatch_on_init(self, host_object: Any) -> None:
169
- """For internal use only."""
170
- self._restore_persistent_properties(host_object)
171
- create_dispatch(self)
172
- self.on_init()
168
+ def _dispatch_on_init(self, host_object: Any) -> None:
169
+ """For internal use only."""
170
+ self._log_custom_init_warning()
171
+ self._restore_persistent_properties(host_object)
172
+ create_dispatch(self)
173
+ self.on_init()
173
174
  self._save_persistent_properties(host_object)
174
175
  return
175
176
 
@@ -34,11 +34,12 @@ class _BusinessService(_BusinessHost):
34
34
  Adapter = adapter = None
35
35
  _wait_for_next_call_interval = False
36
36
 
37
- def _dispatch_on_init(self, host_object) -> None:
38
- """For internal use only."""
39
- self.on_init()
40
-
41
- return
37
+ def _dispatch_on_init(self, host_object) -> None:
38
+ """For internal use only."""
39
+ self._log_custom_init_warning()
40
+ self.on_init()
41
+
42
+ return
42
43
 
43
44
  def on_message(self, request=None):
44
45
  """Handle a message received by the business service.
@@ -1,7 +1,8 @@
1
1
  import inspect
2
- import traceback
3
- from enum import Enum
4
- from types import UnionType
2
+ import traceback
3
+ import warnings
4
+ from enum import Enum
5
+ from types import UnionType
5
6
  from typing import (
6
7
  Annotated,
7
8
  Any,
@@ -133,8 +134,17 @@ def _is_setting_member(name: str, value: Any) -> bool:
133
134
  return not (
134
135
  inspect.ismethod(value) or inspect.isfunction(value) or inspect.isclass(value)
135
136
  )
136
-
137
-
137
+
138
+
139
+ def _custom_init_owner(cls: type) -> type | None:
140
+ for base in inspect.getmro(cls):
141
+ init = base.__dict__.get("__init__", _NO_VALUE)
142
+ if init is _NO_VALUE or init is object.__init__:
143
+ continue
144
+ return base
145
+ return None
146
+
147
+
138
148
  class _Common:
139
149
  """Base class that defines common methods for all component types.
140
150
 
@@ -144,13 +154,52 @@ class _Common:
144
154
 
145
155
  INFO_URL: ClassVar[str]
146
156
  ICON_URL: ClassVar[str]
147
- iris_handle: Any = None
148
- _log_to_console: bool = False
149
- _logger: logging.Logger | None = None
150
-
151
- @staticmethod
152
- def get_adapter_type() -> str | None:
153
- """Get the adapter type for this component. Override in subclasses."""
157
+ iris_handle: Any = None
158
+ _log_to_console: bool = False
159
+ _logger: logging.Logger | None = None
160
+
161
+ def __init_subclass__(cls, **kwargs: Any) -> None:
162
+ super().__init_subclass__(**kwargs)
163
+ cls._warn_if_custom_init_defined(stacklevel=2)
164
+
165
+ @classmethod
166
+ def _custom_init_warning_message(cls) -> str | None:
167
+ if _custom_init_owner(cls) is None:
168
+ return None
169
+ classname = f"{cls.__module__}.{cls.__qualname__}"
170
+ return (
171
+ f"{classname} defines or inherits __init__(), but IoP/IRIS "
172
+ "instantiates components with __new__() and does not call "
173
+ "__init__(). Move startup logic to on_init(); use class attributes "
174
+ "or iop.Setting for configurable defaults."
175
+ )
176
+
177
+ @classmethod
178
+ def _warn_if_custom_init_defined(cls, stacklevel: int = 2) -> None:
179
+ message = cls._custom_init_warning_message()
180
+ if message is None:
181
+ return
182
+ warnings.warn(message, RuntimeWarning, stacklevel=stacklevel)
183
+
184
+ def _log_custom_init_warning(self) -> None:
185
+ message = self.__class__._custom_init_warning_message()
186
+ if message is None:
187
+ return
188
+ try:
189
+ self.log_warning(message)
190
+ except Exception:
191
+ try:
192
+ warnings.warn(message, RuntimeWarning, stacklevel=2)
193
+ except Exception:
194
+ pass
195
+
196
+ def _warn_if_custom_init(self) -> None:
197
+ """Metadata-safe warning hook for ObjectScript __new__ allocations."""
198
+ self._log_custom_init_warning()
199
+
200
+ @staticmethod
201
+ def get_adapter_type() -> str | None:
202
+ """Get the adapter type for this component. Override in subclasses."""
154
203
  return None
155
204
 
156
205
  @property
@@ -191,9 +240,10 @@ class _Common:
191
240
  def _dispatch_on_connected(self, host_object: Any) -> None:
192
241
  self.on_connected()
193
242
 
194
- def _dispatch_on_init(self, host_object: Any) -> None:
195
- """Initialize component when started."""
196
- self.on_init()
243
+ def _dispatch_on_init(self, host_object: Any) -> None:
244
+ """Initialize component when started."""
245
+ self._log_custom_init_warning()
246
+ self.on_init()
197
247
 
198
248
  def _dispatch_on_tear_down(self, host_object: Any) -> None:
199
249
  self.on_tear_down()
@@ -3,6 +3,7 @@ from functools import wraps
3
3
  from typing import Any
4
4
 
5
5
  from .dispatch import dispatch_deserializer, dispatch_serializer
6
+ from .dispatch import handler as handler
6
7
 
7
8
 
8
9
  def input_serializer(fonction: Callable) -> Callable:
@@ -1,3 +1,6 @@
1
+ import logging
2
+ from collections.abc import Callable
3
+ from dataclasses import dataclass
1
4
  from inspect import Parameter, signature
2
5
  from typing import Any
3
6
 
@@ -29,6 +32,34 @@ _PICKLE_MESSAGE_CLASSES = {
29
32
  "IOP.PickleMessage",
30
33
  "IOP.Generator.Message.StartPickle",
31
34
  }
35
+ _HANDLER_ATTRIBUTE = "__iop_handler_message__"
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class _DispatchCandidate:
40
+ message: str
41
+ method: str
42
+ source: str
43
+ priority: int
44
+ index: int
45
+
46
+
47
+ def handler(message_type: Any) -> Callable[[Callable], Callable]:
48
+ """Declare a method as the handler for a message type.
49
+
50
+ Args:
51
+ message_type: The message class, fully qualified class name string, or
52
+ IRIS object instance this method handles.
53
+ """
54
+ message = _message_class_name(message_type)
55
+ if message is None:
56
+ raise TypeError("handler() requires a message class or class name")
57
+
58
+ def _handler(method: Callable) -> Callable:
59
+ setattr(method, _HANDLER_ATTRIBUTE, message)
60
+ return method
61
+
62
+ return _handler
32
63
 
33
64
 
34
65
  def dispatch_serializer(message: Any, is_generator: bool = False) -> Any:
@@ -119,7 +150,7 @@ def dispatch_message(host: Any, request: Any) -> Any:
119
150
 
120
151
  for msg, method in host.DISPATCH:
121
152
  if msg == module + "." + classname:
122
- call = method
153
+ return getattr(host, method)(request)
123
154
 
124
155
  return getattr(host, call)(request)
125
156
 
@@ -129,14 +160,35 @@ def create_dispatch(host: Any) -> None:
129
160
  The dispatch table consists of tuples of (fully_qualified_class_name, method_name).
130
161
  Only methods that take a single typed parameter are considered as handlers.
131
162
  """
132
- dispatch = _declared_dispatch(host)
163
+ candidates: list[_DispatchCandidate] = []
164
+ index = 0
165
+
166
+ for message, method in _decorated_dispatch(host):
167
+ candidates.append(
168
+ _DispatchCandidate(message, method, "@handler", priority=0, index=index)
169
+ )
170
+ index += 1
171
+
172
+ for message, method in _declared_dispatch(host):
173
+ candidates.append(
174
+ _DispatchCandidate(message, method, "DISPATCH", priority=1, index=index)
175
+ )
176
+ index += 1
133
177
 
134
178
  for method_name in get_callable_methods(host):
179
+ if _handler_message(getattr(host, method_name)) is not None:
180
+ continue
135
181
  handler_info = get_handler_info(host, method_name)
136
- if handler_info and handler_info not in dispatch:
137
- dispatch.append(handler_info)
182
+ if handler_info:
183
+ message, method = handler_info
184
+ candidates.append(
185
+ _DispatchCandidate(
186
+ message, method, "typed method", priority=1, index=index
187
+ )
188
+ )
189
+ index += 1
138
190
 
139
- host.DISPATCH = dispatch
191
+ host.DISPATCH = _deduplicate_dispatch(host, candidates)
140
192
 
141
193
 
142
194
  def _declared_dispatch(host: Any) -> list[tuple[str, str]]:
@@ -150,6 +202,82 @@ def _declared_dispatch(host: Any) -> list[tuple[str, str]]:
150
202
  return []
151
203
 
152
204
 
205
+ def _decorated_dispatch(host: Any) -> list[tuple[str, str]]:
206
+ dispatch = []
207
+ for method_name in dir(host):
208
+ method = getattr(host, method_name)
209
+ if not callable(method):
210
+ continue
211
+ message = _handler_message(method)
212
+ if message is not None:
213
+ dispatch.append((message, method_name))
214
+ return dispatch
215
+
216
+
217
+ def _deduplicate_dispatch(
218
+ host: Any, candidates: list[_DispatchCandidate]
219
+ ) -> list[tuple[str, str]]:
220
+ selected: dict[str, _DispatchCandidate] = {}
221
+
222
+ for candidate in candidates:
223
+ current = selected.get(candidate.message)
224
+ if current is None:
225
+ selected[candidate.message] = candidate
226
+ continue
227
+
228
+ if current.method == candidate.method:
229
+ if _is_higher_priority(candidate, current):
230
+ selected[candidate.message] = candidate
231
+ continue
232
+
233
+ if _is_higher_priority(candidate, current):
234
+ _log_duplicate_mapping(host, kept=candidate, discarded=current)
235
+ selected[candidate.message] = candidate
236
+ else:
237
+ _log_duplicate_mapping(host, kept=current, discarded=candidate)
238
+
239
+ return [
240
+ (candidate.message, candidate.method)
241
+ for candidate in sorted(selected.values(), key=lambda item: item.index)
242
+ ]
243
+
244
+
245
+ def _is_higher_priority(
246
+ candidate: _DispatchCandidate, current: _DispatchCandidate
247
+ ) -> bool:
248
+ if candidate.priority != current.priority:
249
+ return candidate.priority < current.priority
250
+ return candidate.index > current.index
251
+
252
+
253
+ def _log_duplicate_mapping(
254
+ host: Any, kept: _DispatchCandidate, discarded: _DispatchCandidate
255
+ ) -> None:
256
+ message = (
257
+ f"Duplicate dispatch mapping for {kept.message}: "
258
+ f"keeping {kept.method} from {kept.source}; "
259
+ f"discarding {discarded.method} from {discarded.source}."
260
+ )
261
+ log_warning = getattr(host, "log_warning", None)
262
+ if callable(log_warning):
263
+ try:
264
+ log_warning(message)
265
+ return
266
+ except Exception:
267
+ pass
268
+
269
+ logging.getLogger(__name__).warning(message)
270
+
271
+
272
+ def _handler_message(method: Any) -> str | None:
273
+ message = getattr(method, _HANDLER_ATTRIBUTE, None)
274
+ if message is not None:
275
+ return message
276
+
277
+ func = getattr(method, "__func__", None)
278
+ return getattr(func, _HANDLER_ATTRIBUTE, None)
279
+
280
+
153
281
  def get_callable_methods(host: Any) -> list[str]:
154
282
  """Returns a list of callable method names that don't start with underscore."""
155
283
  return [
@@ -172,20 +300,27 @@ def get_handler_info(host: Any, method_name: str) -> tuple[str, str] | None:
172
300
  param: Parameter = next(iter(params.values()))
173
301
  annotation = param.annotation
174
302
 
175
- if isinstance(annotation, str):
176
- # return it as is, assuming it's a fully qualified class name
177
- return annotation, method_name
178
-
179
- if is_iris_object_instance(annotation):
180
- return (
181
- f"{type(annotation).__module__}.{type(annotation).__name__}",
182
- method_name,
183
- )
303
+ if annotation == Parameter.empty:
304
+ return None
184
305
 
185
- if annotation == Parameter.empty or not isinstance(annotation, type):
306
+ message = _message_class_name(annotation)
307
+ if message is None:
186
308
  return None
187
309
 
188
- return f"{annotation.__module__}.{annotation.__name__}", method_name
310
+ return message, method_name
189
311
 
190
312
  except ValueError:
191
313
  return None
314
+
315
+
316
+ def _message_class_name(message_type: Any) -> str | None:
317
+ if isinstance(message_type, str):
318
+ return message_type
319
+
320
+ if is_iris_object_instance(message_type):
321
+ return f"{type(message_type).__module__}.{type(message_type).__name__}"
322
+
323
+ if not isinstance(message_type, type):
324
+ return None
325
+
326
+ return f"{message_type.__module__}.{message_type.__name__}"