funcnodes-core 2.2.0__tar.gz → 2.3.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 (91) hide show
  1. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/CHANGELOG.md +18 -0
  2. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/PKG-INFO +1 -1
  3. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/pyproject.toml +1 -1
  4. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/_setup.py +13 -14
  5. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/node.py +27 -3
  6. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/files.py +4 -2
  7. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/plugins.py +38 -10
  8. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/plugins_types.py +46 -1
  9. funcnodes_core-2.3.0/tests/conftest.py +39 -0
  10. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodeutils.py +40 -0
  11. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_triggering.py +5 -33
  12. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_plugins.py +14 -11
  13. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/uv.lock +1 -1
  14. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.coveragerc +0 -0
  15. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.flake8 +0 -0
  16. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.github/actions/install_package/action.yml +0 -0
  17. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.github/workflows/py_test.yml +0 -0
  18. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.github/workflows/version_publish_main.yml +0 -0
  19. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.gitignore +0 -0
  20. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.pre-commit-config.yaml +0 -0
  21. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/LICENSE +0 -0
  22. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/MANIFEST.in +0 -0
  23. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/README.md +0 -0
  24. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/THIRD_PARTY_NOTICES.md +0 -0
  25. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/conftest.py +0 -0
  26. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/pytest.ini +0 -0
  27. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/__init__.py +0 -0
  28. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/_logging.py +0 -0
  29. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/config.py +0 -0
  30. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/data.py +0 -0
  31. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/datapath.py +0 -0
  32. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/decorator/__init__.py +0 -0
  33. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/eventmanager.py +0 -0
  34. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/exceptions.py +0 -0
  35. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/graph.py +0 -0
  36. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/grouping_logic.py +0 -0
  37. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/io.py +0 -0
  38. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/lib/__init__.py +0 -0
  39. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/lib/lib.py +0 -0
  40. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/lib/libfinder.py +0 -0
  41. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/lib/libparser.py +0 -0
  42. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/nodemaker.py +0 -0
  43. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/nodespace.py +0 -0
  44. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/testing.py +0 -0
  45. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/triggerstack.py +0 -0
  46. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/__init__.py +0 -0
  47. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/cache.py +0 -0
  48. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/data.py +0 -0
  49. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/deprecations.py +0 -0
  50. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/functions.py +0 -0
  51. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/modules.py +0 -0
  52. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/nodetqdm.py +0 -0
  53. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/nodeutils.py +0 -0
  54. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/saving.py +0 -0
  55. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/serialization.py +0 -0
  56. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/special_types.py +0 -0
  57. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/wrapper.py +0 -0
  58. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/__init__.py +0 -0
  59. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_cache_utils.py +0 -0
  60. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_config.py +0 -0
  61. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_data.py +0 -0
  62. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_datapaths.py +0 -0
  63. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_decorator.py +0 -0
  64. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_eventmanager.py +0 -0
  65. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_exceptions.py +0 -0
  66. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_functions.py +0 -0
  67. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_graph.py +0 -0
  68. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_grouping_logic.py +0 -0
  69. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_lib.py +0 -0
  70. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_libfinder.py +0 -0
  71. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodeclass.py +0 -0
  72. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodeclassmixin.py +0 -0
  73. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodeio.py +0 -0
  74. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodemaker.py +0 -0
  75. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodespace.py +0 -0
  76. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_public_api.py +0 -0
  77. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_setup.py +0 -0
  78. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_triggerstack.py +0 -0
  79. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/__init__.py +0 -0
  80. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_datautils.py +0 -0
  81. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_deprecations.py +0 -0
  82. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_files.py +0 -0
  83. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_logging.py +0 -0
  84. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_modules.py +0 -0
  85. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_nodetqdm.py +0 -0
  86. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_plugins_types.py +0 -0
  87. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_saving.py +0 -0
  88. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_serialization.py +0 -0
  89. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_special_types.py +0 -0
  90. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_testing.py +0 -0
  91. {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_wrapper.py +0 -0
@@ -1,3 +1,21 @@
1
+ ## v2.3.0 (2025-12-23)
2
+
3
+ ### Feat
4
+
5
+ - **node**: enhance trigger handling with asyncio task management
6
+ - **tests**: add yappi profiling context manager and integrate into test cases
7
+ - **plugins**: add JSON representation method to InstalledModule
8
+
9
+ ### Fix
10
+
11
+ - **files**: ensure temporary file cleanup on error in write_json_secure
12
+
13
+ ### Refactor
14
+
15
+ - **node, plugins**: improve event handling and entry point validation
16
+ - **plugins**: enhance module handling and entry point loading
17
+ - **setup**: improve error logging and simplify entry point handling
18
+
1
19
  ## v2.2.0 (2025-12-12)
2
20
 
3
21
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funcnodes-core
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: core package for funcnodes
5
5
  Project-URL: homepage, https://github.com/Linkdlab/funcnodes_core
6
6
  Project-URL: source, https://github.com/Linkdlab/funcnodes_core
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "funcnodes-core"
3
3
 
4
- version = "2.2.0"
4
+ version = "2.3.0"
5
5
 
6
6
  description = "core package for funcnodes"
7
7
  authors = [{name = "Julian Kimmig", email = "julian.kimmig@linkdlab.de"}]
@@ -1,7 +1,6 @@
1
1
  from typing import Dict, Optional
2
2
  import gc
3
3
  from .config import update_render_options
4
- from .lib import check_shelf
5
4
  from ._logging import FUNCNODES_LOGGER
6
5
  from .utils.plugins import get_installed_modules, InstalledModule, PLUGIN_FUNCTIONS
7
6
 
@@ -10,8 +9,8 @@ def setup_module(mod_data: InstalledModule) -> Optional[InstalledModule]:
10
9
  gc.collect()
11
10
  entry_points = mod_data.entry_points
12
11
  mod = mod_data.module
13
- if not mod: # funcnodes modules must have an module entry point
14
- return None
12
+ # if not mod: # funcnodes modules must have an module entry point
13
+ # return None
15
14
 
16
15
  # first we try to register the plugin setup function as this might register other functions
17
16
  try:
@@ -21,7 +20,7 @@ def setup_module(mod_data: InstalledModule) -> Optional[InstalledModule]:
21
20
  mod.FUNCNODES_PLUGIN_SETUP()
22
21
  entry_points["render_options"] = mod.FUNCNODES_PLUGIN_SETUP
23
22
  except Exception as e:
24
- FUNCNODES_LOGGER.error("Error in plugin setup %s: %s" % (mod.__name__, e))
23
+ FUNCNODES_LOGGER.error("Error in plugin setup %s: %s" % (mod_data.name, e))
25
24
 
26
25
  # Then we call the plugin functions
27
26
  for pluginf in PLUGIN_FUNCTIONS.values():
@@ -29,7 +28,7 @@ def setup_module(mod_data: InstalledModule) -> Optional[InstalledModule]:
29
28
  pluginf(mod_data)
30
29
  except Exception as e:
31
30
  FUNCNODES_LOGGER.error(
32
- "Error in setup_module plugin function %s: %s" % (mod.__name__, e)
31
+ "Error in setup_module plugin function %s: %s" % (mod_data.name, e)
33
32
  )
34
33
 
35
34
  if "render_options" in entry_points:
@@ -48,15 +47,15 @@ def setup_module(mod_data: InstalledModule) -> Optional[InstalledModule]:
48
47
  if hasattr(mod, sn):
49
48
  entry_points["shelf"] = getattr(mod, sn)
50
49
  break
51
- if "shelf" in entry_points:
52
- try:
53
- entry_points["shelf"] = check_shelf(
54
- entry_points["shelf"], parent_id=mod_data.name
55
- )
56
- except ValueError as e:
57
- FUNCNODES_LOGGER.error("Error in module %s: %s" % (mod.__name__, e))
58
- del entry_points["shelf"]
59
- mod_data._is_setup = True
50
+ # if "shelf" in entry_points:
51
+ # try:
52
+ # entry_points["shelf"] = check_shelf(
53
+ # entry_points["shelf"], parent_id=mod_data.name
54
+ # )
55
+ # except ValueError as e:
56
+ # FUNCNODES_LOGGER.error("Error in module %s: %s" % (mod_data.name, e))
57
+ # del entry_points["shelf"]
58
+ mod_data._is_setup = mod is not None
60
59
  return mod_data
61
60
 
62
61
 
@@ -400,6 +400,7 @@ class Node(NoOverrideMixin, EventEmitterMixin, ABC, metaclass=NodeMeta):
400
400
  self._outputs: List[NodeOutput] = []
401
401
  self._outputs_dict: Optional[Dict[str, NodeOutput]] = None
402
402
  self._triggerstack: Optional[TriggerStack] = None
403
+ self._trigger_task: Optional[asyncio.Task] = None
403
404
  # flag whether the trigger has started but still not read the ios
404
405
  self._trigger_open = False
405
406
  self._requests_trigger = False
@@ -724,6 +725,8 @@ class Node(NoOverrideMixin, EventEmitterMixin, ABC, metaclass=NodeMeta):
724
725
  checked if the triggerstack is not None and if it is done
725
726
  or if the _trigger_open flag is set
726
727
  """
728
+ if self._trigger_task is not None and self._trigger_task.done():
729
+ self._trigger_task = None
727
730
  if self._triggerstack is not None:
728
731
  if self._triggerstack.done():
729
732
  self._triggerstack = None
@@ -1100,8 +1103,26 @@ class Node(NoOverrideMixin, EventEmitterMixin, ABC, metaclass=NodeMeta):
1100
1103
 
1101
1104
  @savemethod
1102
1105
  async def wait_for_trigger_finish(self):
1103
- if self.in_trigger:
1104
- await self.asynceventmanager.wait("triggerdone")
1106
+ while self.in_trigger:
1107
+ task = self._trigger_task
1108
+ if task is not None:
1109
+ try:
1110
+ # Wait on the trigger task directly to avoid missing event pulses.
1111
+ await asyncio.wait_for(asyncio.shield(task), timeout=0.5)
1112
+ except asyncio.TimeoutError:
1113
+ continue
1114
+ else:
1115
+ try:
1116
+ # Fallback when no task reference is available.
1117
+ await asyncio.wait_for(
1118
+ self.asynceventmanager.wait("triggerdone"), timeout=0.5
1119
+ )
1120
+ except asyncio.TimeoutError:
1121
+ continue
1122
+
1123
+ def _clear_trigger_task(self, task: asyncio.Task) -> None:
1124
+ if self._trigger_task is task:
1125
+ self._trigger_task = None
1105
1126
 
1106
1127
  @savemethod
1107
1128
  async def await_until_complete(self):
@@ -1145,7 +1166,10 @@ class Node(NoOverrideMixin, EventEmitterMixin, ABC, metaclass=NodeMeta):
1145
1166
  triggerlogger.debug("triggering %s", self)
1146
1167
  self._trigger_open = True
1147
1168
  self._triggerstack = triggerstack
1148
- self._triggerstack.append(asyncio.create_task(self()))
1169
+ task = asyncio.create_task(self())
1170
+ self._trigger_task = task
1171
+ task.add_done_callback(self._clear_trigger_task)
1172
+ self._triggerstack.append(task)
1149
1173
  self._requests_trigger = False
1150
1174
  return self._triggerstack
1151
1175
 
@@ -22,6 +22,7 @@ def write_json_secure(data, filepath: Union[Path, str], cls=None, **kwargs):
22
22
  cls = cls or JSONEncoder
23
23
 
24
24
  # Create a temporary file in the same directory
25
+ temp_file_path = None
25
26
  try:
26
27
  with tempfile.NamedTemporaryFile(
27
28
  "w+", dir=directory, delete=False, encoding="utf-8"
@@ -33,8 +34,9 @@ def write_json_secure(data, filepath: Union[Path, str], cls=None, **kwargs):
33
34
  os.fsync(temp_file.fileno()) # Force writing to disk for durability
34
35
  except Exception as e:
35
36
  # Clean up the temporary file in case of an error
36
- if os.path.exists(temp_file_path):
37
- os.remove(temp_file_path)
37
+ if temp_file_path:
38
+ if os.path.exists(temp_file_path):
39
+ os.remove(temp_file_path)
38
40
  raise e
39
41
 
40
42
  # Atomically replace the target file with the temporary file
@@ -1,6 +1,11 @@
1
1
  from typing import Dict, Optional
2
2
  from collections.abc import Callable
3
- from importlib.metadata import entry_points, Distribution, PackageNotFoundError
3
+ from importlib.metadata import (
4
+ EntryPoint,
5
+ entry_points,
6
+ Distribution,
7
+ PackageNotFoundError,
8
+ )
4
9
  from importlib import reload
5
10
  import sys
6
11
  from .._logging import FUNCNODES_LOGGER
@@ -44,13 +49,15 @@ def reload_plugin_module(module_name: str):
44
49
 
45
50
  def assert_entry_points_loaded(modulde_data: InstalledModule):
46
51
  for ep in entry_points(group="funcnodes.module", module=modulde_data.name):
47
- if ep.name in modulde_data.entry_points:
52
+ if ep.name in modulde_data.entry_points and not isinstance(
53
+ modulde_data.entry_points[ep.name], EntryPoint
54
+ ):
48
55
  continue
49
56
  try:
50
57
  loaded = ep.load()
51
58
  modulde_data.entry_points[ep.name] = loaded
52
59
  if ep.name == "module":
53
- modulde_data.module = loaded
60
+ modulde_data.set_module(loaded)
54
61
  except Exception as exc:
55
62
  FUNCNODES_LOGGER.exception(f"Failed to load entry point {ep.name}: {exc}")
56
63
 
@@ -106,24 +113,45 @@ def assert_module_metadata(modulde_data: InstalledModule):
106
113
  return modulde_data
107
114
 
108
115
 
116
+ def setup_plugin_module(module_name: str) -> Optional[InstalledModule]:
117
+ modulde_data = InstalledModule(
118
+ name=module_name,
119
+ entry_points={},
120
+ module=None, # module not directly added since only modules with a module entry point are relevant
121
+ )
122
+
123
+ for ep in entry_points(group="funcnodes.module", module=modulde_data.name):
124
+ if ep.name in modulde_data.entry_points:
125
+ continue
126
+ modulde_data.entry_points[ep.name] = ep
127
+
128
+ return modulde_data
129
+
130
+
109
131
  def get_installed_modules(
110
132
  named_objects: Optional[Dict[str, InstalledModule]] = None,
111
133
  ) -> Dict[str, InstalledModule]:
112
134
  if named_objects is None:
113
135
  named_objects: Dict[str, InstalledModule] = {}
114
136
 
115
- modules = set()
116
-
117
137
  for ep in entry_points(group="funcnodes.module"):
118
138
  module_name = ep.module
119
- modules.add(module_name)
120
- for module_name in modules:
121
- if module_name not in named_objects:
139
+ if module_name in named_objects:
140
+ continue
141
+ # insmod = setup_plugin_module(module_name)
142
+ # if not insmod:
143
+ # continue
144
+ # named_objects[module_name] = insmod
145
+
146
+ # old code
147
+ if module_name in sys.modules:
122
148
  named_objects[module_name] = reload_plugin_module(module_name)
149
+ else:
150
+ named_objects[module_name] = setup_plugin_module(module_name)
123
151
  modulde_data = named_objects[module_name]
124
- modulde_data = assert_entry_points_loaded(modulde_data)
125
- modulde_data = assert_module_metadata(modulde_data)
126
152
 
153
+ for module_name, modulde_data in named_objects.items():
154
+ modulde_data = assert_module_metadata(modulde_data)
127
155
  return named_objects
128
156
 
129
157
 
@@ -1,5 +1,6 @@
1
1
  from typing import Dict, Any, Optional, TypedDict, List
2
2
  from dataclasses import dataclass, field
3
+ from importlib.metadata import EntryPoint
3
4
 
4
5
 
5
6
  class RenderOptions(TypedDict, total=False):
@@ -27,6 +28,29 @@ class BasePlugin(TypedDict):
27
28
  module: str
28
29
 
29
30
 
31
+ class _LazyEntryDict(dict):
32
+ def __getitem__(self, name: str) -> Any:
33
+ value = super().__getitem__(name)
34
+ if isinstance(value, EntryPoint):
35
+ value = value.load()
36
+ self[name] = value
37
+ if hasattr(self, "installed_module"):
38
+ installed_module = getattr(self, "installed_module")
39
+ if installed_module.module is None:
40
+ if name == "module":
41
+ installed_module.set_module(value)
42
+ else:
43
+ module = self.get("module", None)
44
+ if module is not None:
45
+ installed_module.set_module(module)
46
+ return value
47
+
48
+ def get(self, name: str, default: Any = None) -> Any:
49
+ if name not in self:
50
+ return default
51
+ return self[name]
52
+
53
+
30
54
  @dataclass
31
55
  class InstalledModule:
32
56
  """
@@ -40,12 +64,18 @@ class InstalledModule:
40
64
  name: str
41
65
  module: Any
42
66
  description: Optional[str] = None
43
- entry_points: Dict[str, Any] = field(default_factory=dict)
67
+ entry_points: Dict[str, Any] = field(default_factory=_LazyEntryDict)
44
68
  plugins: List[BasePlugin] = field(default_factory=list)
45
69
  render_options: Optional[RenderOptions] = None
46
70
  version: Optional[str] = None
47
71
  _is_setup = False
48
72
 
73
+ # make sure that entrz points is a _LazyEntryDict
74
+ def __post_init__(self):
75
+ if not isinstance(self.entry_points, _LazyEntryDict):
76
+ self.entry_points = _LazyEntryDict(self.entry_points)
77
+ self.entry_points.installed_module = self
78
+
49
79
  @property
50
80
  def rep_dict(self) -> dict[str, Any]:
51
81
  return {
@@ -62,3 +92,18 @@ class InstalledModule:
62
92
 
63
93
  def __str__(self) -> str:
64
94
  return self.__repr__()
95
+
96
+ def set_module(self, module: Any):
97
+ if self.module is not None:
98
+ if self.module != module:
99
+ raise ValueError(
100
+ f"Module {self.name} already has a module {self.module} and cannot be set to {module}"
101
+ )
102
+ self.module = module
103
+ if not self._is_setup:
104
+ from .._setup import setup_module
105
+
106
+ setup_module(self)
107
+
108
+ def _repr_json_(self) -> dict[str, Any]:
109
+ return self.rep_dict
@@ -0,0 +1,39 @@
1
+ import pathlib
2
+ import os
3
+ import pytest
4
+
5
+ try:
6
+ import yappi
7
+ except ImportError:
8
+ yappi = None
9
+
10
+
11
+ class _yappicontext:
12
+ def __init__(self, file):
13
+ base_dir = pathlib.Path(
14
+ os.environ.get("TEST_OUTPUT_DIR", "testouts")
15
+ ).absolute()
16
+ if not base_dir.exists():
17
+ base_dir.mkdir(parents=True, exist_ok=True)
18
+ self.file = str(base_dir / file)
19
+
20
+ def __enter__(self):
21
+ if yappi is not None:
22
+ yappi.set_clock_type("WALL")
23
+ yappi.start()
24
+
25
+ def __exit__(self, exc_type, exc_val, exc_tb):
26
+ if yappi is not None:
27
+ yappi.stop()
28
+ yappi.get_func_stats().save(self.file, "pstat")
29
+ yappi.clear_stats()
30
+
31
+
32
+ @pytest.fixture
33
+ def yappicontext_class():
34
+ return _yappicontext
35
+
36
+
37
+ @pytest.fixture
38
+ def yappicontext(yappicontext_class, request):
39
+ return yappicontext_class(request.node.name + ".pstat")
@@ -8,6 +8,7 @@ from funcnodes_core.utils.nodeutils import (
8
8
  run_until_complete,
9
9
  )
10
10
  from funcnodes_core.nodemaker import NodeDecorator
11
+ from funcnodes_core.eventmanager import AsyncEventManager
11
12
 
12
13
  import funcnodes_core as fn
13
14
 
@@ -37,6 +38,26 @@ class TKNode(fn.Node):
37
38
  self.outputs["op1"].value += 1
38
39
 
39
40
 
41
+ class SlowClearEventManager(AsyncEventManager):
42
+ def __init__(self, obj):
43
+ super().__init__(obj)
44
+ self.triggerdone_cleared = asyncio.Event()
45
+
46
+ async def set_and_clear(self, event: str, delta: float = 0) -> None:
47
+ await super().set_and_clear(event, delta=delta)
48
+ if event == "triggerdone":
49
+ self.triggerdone_cleared.set()
50
+ await asyncio.sleep(0.05)
51
+
52
+
53
+ class SlowEventTKNode(TKNode):
54
+ node_id = "slow_event_tknode"
55
+
56
+ def __init__(self, *args, **kwargs):
57
+ super().__init__(*args, **kwargs)
58
+ self.asynceventmanager = SlowClearEventManager(self)
59
+
60
+
40
61
  @dataclass
41
62
  class NodeChain:
42
63
  node1: fn.Node
@@ -181,3 +202,22 @@ async def test_trigger_fast():
181
202
  tw1 = te1 - ts1
182
203
  assert tw1 < 0.5
183
204
  assert node.outputs["op1"].value == 101
205
+
206
+
207
+ @funcnodes_test
208
+ async def test_wait_for_trigger_finish_avoids_triggerdone_race():
209
+ node = SlowEventTKNode()
210
+ node.pretrigger_delay = 0.0
211
+ node.inputs["ip1"].value = 1
212
+ node.inputs["ip2"].value = 2
213
+ node.request_trigger()
214
+
215
+ await node.asynceventmanager.triggerdone_cleared.wait()
216
+ assert node.in_trigger
217
+
218
+ ts = time.perf_counter()
219
+ await node.wait_for_trigger_finish()
220
+ tw = time.perf_counter() - ts
221
+
222
+ assert tw < 0.2
223
+ assert node.outputs["op1"].value == 1
@@ -1,39 +1,11 @@
1
1
  import time
2
- import os
3
- import pathlib
4
2
 
5
3
  import funcnodes_core as fn
6
4
  from pytest_funcnodes import funcnodes_test
7
5
 
8
- try:
9
- import yappi
10
- except ImportError:
11
- yappi = None
12
-
13
-
14
- class yappicontext:
15
- def __init__(self, file):
16
- base_dir = pathlib.Path(
17
- os.environ.get("TEST_OUTPUT_DIR", "testouts")
18
- ).absolute()
19
- if not base_dir.exists():
20
- base_dir.mkdir(parents=True, exist_ok=True)
21
- self.file = str(base_dir / file)
22
-
23
- def __enter__(self):
24
- if yappi is not None:
25
- yappi.set_clock_type("WALL")
26
- yappi.start()
27
-
28
- def __exit__(self, exc_type, exc_val, exc_tb):
29
- if yappi is not None:
30
- yappi.stop()
31
- yappi.get_func_stats().save(self.file, "pstat")
32
- yappi.clear_stats()
33
-
34
6
 
35
7
  @funcnodes_test
36
- async def test_triggerspeeds():
8
+ async def test_triggerspeeds(yappicontext_class):
37
9
  @fn.NodeDecorator("TestTriggerSpeed test_triggerspeeds")
38
10
  async def _add_one(input: int) -> int:
39
11
  return input + 1 # a very simple and fast operation
@@ -43,13 +15,13 @@ async def test_triggerspeeds():
43
15
 
44
16
  node = _add_one(pretrigger_delay=0.0)
45
17
 
46
- with yappicontext("test_triggerspeeds_directfunc.pstat"):
18
+ with yappicontext_class("test_triggerspeeds_directfunc.pstat"):
47
19
  t = time.perf_counter()
48
20
  cound_directfunc = 0
49
21
  while time.perf_counter() - t < 1:
50
22
  cound_directfunc = await node.func(cound_directfunc)
51
23
 
52
- with yappicontext("test_triggerspeeds_simplefunc.pstat"):
24
+ with yappicontext_class("test_triggerspeeds_simplefunc.pstat"):
53
25
  t = time.perf_counter()
54
26
  count_simplefunc = 0
55
27
  while time.perf_counter() - t < 1:
@@ -78,7 +50,7 @@ async def test_triggerspeeds():
78
50
 
79
51
  node.on("triggerstart", increase_called_trigger)
80
52
  node.on("triggerfast", increase_called_triggerfast)
81
- with yappicontext("test_triggerspeeds_direct_called.pstat"):
53
+ with yappicontext_class("test_triggerspeeds_direct_called.pstat"):
82
54
  while time.perf_counter() - t < 1:
83
55
  await node()
84
56
  node.inputs["input"].value = node.outputs["out"].value
@@ -93,7 +65,7 @@ async def test_triggerspeeds():
93
65
  trigger_direct_called > cound_directfunc / 5
94
66
  ) # overhead due to all the trigger set and clear
95
67
 
96
- with yappicontext("test_triggerspeeds_called_await.pstat"):
68
+ with yappicontext_class("test_triggerspeeds_called_await.pstat"):
97
69
  node.inputs["input"].value = 1
98
70
 
99
71
  t = time.perf_counter()
@@ -245,17 +245,24 @@ def test_get_installed_modules_deduplicates_and_updates(monkeypatch):
245
245
  """get_installed_modules should reload missing entries once and enrich data."""
246
246
 
247
247
  class GroupEntryPoint:
248
- def __init__(self, module):
248
+ def __init__(self, module, name):
249
249
  self.module = module
250
+ self.name = name
250
251
 
251
252
  group_eps = [
252
- GroupEntryPoint("alpha"),
253
- GroupEntryPoint("beta"),
254
- GroupEntryPoint("alpha"), # duplicate to ensure deduplication
253
+ GroupEntryPoint("alpha", "alpha"),
254
+ GroupEntryPoint("beta", "beta"),
255
+ GroupEntryPoint("alpha", "alpha2"), # duplicate to ensure deduplication
255
256
  ]
256
257
 
257
258
  def fake_entry_points(**kwargs):
258
- assert kwargs == {"group": "funcnodes.module"}
259
+ assert kwargs["group"] == "funcnodes.module"
260
+ if "module" in kwargs:
261
+ if kwargs["module"] == "alpha":
262
+ return [group_eps[0]]
263
+ if kwargs["module"] == "beta":
264
+ return [group_eps[1]]
265
+ raise ValueError(f"Unknown module: {kwargs['module']}")
259
266
  return group_eps
260
267
 
261
268
  reloaded = []
@@ -276,19 +283,14 @@ def test_get_installed_modules_deduplicates_and_updates(monkeypatch):
276
283
 
277
284
  monkeypatch.setattr(plugins_module, "entry_points", fake_entry_points)
278
285
  monkeypatch.setattr(plugins_module, "reload_plugin_module", fake_reload)
279
- monkeypatch.setattr(
280
- plugins_module, "assert_entry_points_loaded", fake_assert_entry_points_loaded
281
- )
286
+
282
287
  monkeypatch.setattr(
283
288
  plugins_module, "assert_module_metadata", fake_assert_module_metadata
284
289
  )
285
290
 
286
291
  result = plugins_module.get_installed_modules(named_objects={"beta": existing})
287
292
 
288
- assert reloaded == ["alpha"]
289
293
  assert set(result.keys()) == {"alpha", "beta"}
290
- assert result["alpha"].entry_points["loaded"] is True
291
- assert result["beta"].entry_points["loaded"] is True
292
294
  assert result["beta"].description == "desc:beta"
293
295
 
294
296
 
@@ -348,6 +350,7 @@ def test_get_installed_modules_initializes_when_named_objects_missing(monkeypatc
348
350
 
349
351
  assert module_name in result
350
352
  assert reload_calls == [module_name]
353
+ result = plugins_module.get_installed_modules()
351
354
  installed = result[module_name]
352
355
  assert installed.module is module_obj
353
356
  assert installed.entry_points["other"] is not None
@@ -457,7 +457,7 @@ wheels = [
457
457
 
458
458
  [[package]]
459
459
  name = "funcnodes-core"
460
- version = "2.2.0"
460
+ version = "2.3.0"
461
461
  source = { editable = "." }
462
462
  dependencies = [
463
463
  { name = "dill" },
File without changes
File without changes
File without changes