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.
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/CHANGELOG.md +18 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/PKG-INFO +1 -1
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/pyproject.toml +1 -1
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/_setup.py +13 -14
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/node.py +27 -3
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/files.py +4 -2
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/plugins.py +38 -10
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/plugins_types.py +46 -1
- funcnodes_core-2.3.0/tests/conftest.py +39 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodeutils.py +40 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_triggering.py +5 -33
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_plugins.py +14 -11
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/uv.lock +1 -1
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.coveragerc +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.flake8 +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.github/actions/install_package/action.yml +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.github/workflows/py_test.yml +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.github/workflows/version_publish_main.yml +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.gitignore +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/.pre-commit-config.yaml +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/LICENSE +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/MANIFEST.in +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/README.md +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/THIRD_PARTY_NOTICES.md +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/conftest.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/pytest.ini +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/__init__.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/_logging.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/config.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/data.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/datapath.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/decorator/__init__.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/eventmanager.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/exceptions.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/graph.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/grouping_logic.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/io.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/lib/__init__.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/lib/lib.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/lib/libfinder.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/lib/libparser.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/nodemaker.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/nodespace.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/testing.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/triggerstack.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/__init__.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/cache.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/data.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/deprecations.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/functions.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/modules.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/nodetqdm.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/nodeutils.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/saving.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/serialization.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/special_types.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/src/funcnodes_core/utils/wrapper.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/__init__.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_cache_utils.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_config.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_data.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_datapaths.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_decorator.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_eventmanager.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_exceptions.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_functions.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_graph.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_grouping_logic.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_lib.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_libfinder.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodeclass.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodeclassmixin.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodeio.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodemaker.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_nodespace.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_public_api.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_setup.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_triggerstack.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/__init__.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_datautils.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_deprecations.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_files.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_logging.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_modules.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_nodetqdm.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_plugins_types.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_saving.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_serialization.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_special_types.py +0 -0
- {funcnodes_core-2.2.0 → funcnodes_core-2.3.0}/tests/test_utils/test_testing.py +0 -0
- {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,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
|
-
|
|
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" % (
|
|
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" % (
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
mod_data._is_setup =
|
|
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
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
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
|
|
37
|
-
os.
|
|
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
|
|
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.
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|