kailash 0.6.5__py3-none-any.whl → 0.7.0__py3-none-any.whl
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.
- kailash/__init__.py +35 -4
- kailash/adapters/__init__.py +5 -0
- kailash/adapters/mcp_platform_adapter.py +273 -0
- kailash/channels/__init__.py +21 -0
- kailash/channels/api_channel.py +409 -0
- kailash/channels/base.py +271 -0
- kailash/channels/cli_channel.py +661 -0
- kailash/channels/event_router.py +496 -0
- kailash/channels/mcp_channel.py +648 -0
- kailash/channels/session.py +423 -0
- kailash/mcp_server/discovery.py +1 -1
- kailash/middleware/core/agent_ui.py +5 -0
- kailash/middleware/mcp/enhanced_server.py +22 -16
- kailash/nexus/__init__.py +21 -0
- kailash/nexus/factory.py +413 -0
- kailash/nexus/gateway.py +545 -0
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/ai/iterative_llm_agent.py +988 -17
- kailash/nodes/ai/llm_agent.py +29 -9
- kailash/nodes/api/__init__.py +2 -2
- kailash/nodes/api/monitoring.py +1 -1
- kailash/nodes/base_async.py +54 -14
- kailash/nodes/code/async_python.py +1 -1
- kailash/nodes/data/bulk_operations.py +939 -0
- kailash/nodes/data/query_builder.py +373 -0
- kailash/nodes/data/query_cache.py +512 -0
- kailash/nodes/monitoring/__init__.py +10 -0
- kailash/nodes/monitoring/deadlock_detector.py +964 -0
- kailash/nodes/monitoring/performance_anomaly.py +1078 -0
- kailash/nodes/monitoring/race_condition_detector.py +1151 -0
- kailash/nodes/monitoring/transaction_metrics.py +790 -0
- kailash/nodes/monitoring/transaction_monitor.py +931 -0
- kailash/nodes/system/__init__.py +17 -0
- kailash/nodes/system/command_parser.py +820 -0
- kailash/nodes/transaction/__init__.py +48 -0
- kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
- kailash/nodes/transaction/saga_coordinator.py +652 -0
- kailash/nodes/transaction/saga_state_storage.py +411 -0
- kailash/nodes/transaction/saga_step.py +467 -0
- kailash/nodes/transaction/transaction_context.py +756 -0
- kailash/nodes/transaction/two_phase_commit.py +978 -0
- kailash/nodes/transform/processors.py +17 -1
- kailash/nodes/validation/__init__.py +21 -0
- kailash/nodes/validation/test_executor.py +532 -0
- kailash/nodes/validation/validation_nodes.py +447 -0
- kailash/resources/factory.py +1 -1
- kailash/runtime/async_local.py +84 -21
- kailash/runtime/local.py +21 -2
- kailash/runtime/parameter_injector.py +187 -31
- kailash/security.py +16 -1
- kailash/servers/__init__.py +32 -0
- kailash/servers/durable_workflow_server.py +430 -0
- kailash/servers/enterprise_workflow_server.py +466 -0
- kailash/servers/gateway.py +183 -0
- kailash/servers/workflow_server.py +290 -0
- kailash/utils/data_validation.py +192 -0
- kailash/workflow/builder.py +291 -12
- kailash/workflow/validation.py +144 -8
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/METADATA +1 -1
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/RECORD +64 -26
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/WHEEL +0 -0
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/top_level.txt +0 -0
kailash/workflow/builder.py
CHANGED
@@ -2,11 +2,14 @@
|
|
2
2
|
|
3
3
|
import logging
|
4
4
|
import uuid
|
5
|
-
from typing import Any
|
5
|
+
from typing import TYPE_CHECKING, Any
|
6
6
|
|
7
7
|
from kailash.sdk_exceptions import ConnectionError, WorkflowValidationError
|
8
8
|
from kailash.workflow.graph import Workflow
|
9
9
|
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from kailash.nodes.base import Node
|
12
|
+
|
10
13
|
logger = logging.getLogger(__name__)
|
11
14
|
|
12
15
|
|
@@ -19,22 +22,238 @@ class WorkflowBuilder:
|
|
19
22
|
self.connections: list[dict[str, str]] = []
|
20
23
|
self._metadata: dict[str, Any] = {}
|
21
24
|
|
22
|
-
def add_node(
|
25
|
+
def add_node(self, *args, **kwargs) -> str:
|
26
|
+
"""
|
27
|
+
Unified add_node method supporting multiple API patterns.
|
28
|
+
|
29
|
+
Supported patterns:
|
30
|
+
1. add_node("NodeType", "node_id", {"param": value}) # Current/Preferred
|
31
|
+
2. add_node("node_id", NodeClass, param=value) # Legacy fluent
|
32
|
+
3. add_node(NodeClass, "node_id", param=value) # Alternative
|
33
|
+
|
34
|
+
Args:
|
35
|
+
*args: Positional arguments (pattern-dependent)
|
36
|
+
**kwargs: Keyword arguments for configuration
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
Node ID (useful for method chaining)
|
40
|
+
|
41
|
+
Raises:
|
42
|
+
WorkflowValidationError: If node_id is already used or invalid pattern
|
43
|
+
"""
|
44
|
+
# Pattern detection and routing
|
45
|
+
if len(args) == 0 and kwargs:
|
46
|
+
# Keyword-only pattern: add_node(node_type="NodeType", node_id="id", config={})
|
47
|
+
node_type = kwargs.pop("node_type", None)
|
48
|
+
node_id = kwargs.pop("node_id", None)
|
49
|
+
config = kwargs.pop("config", {})
|
50
|
+
# Any remaining kwargs are treated as config
|
51
|
+
config.update(kwargs)
|
52
|
+
|
53
|
+
if node_type is None:
|
54
|
+
raise WorkflowValidationError(
|
55
|
+
"node_type is required when using keyword arguments"
|
56
|
+
)
|
57
|
+
|
58
|
+
return self._add_node_current(node_type, node_id, config)
|
59
|
+
|
60
|
+
elif len(args) == 1:
|
61
|
+
# Single argument with possible keywords
|
62
|
+
if isinstance(args[0], str) and kwargs:
|
63
|
+
# Pattern: add_node("NodeType", node_id="id", config={})
|
64
|
+
node_type = args[0]
|
65
|
+
node_id = kwargs.pop("node_id", None)
|
66
|
+
config = kwargs.pop("config", {})
|
67
|
+
# Any remaining kwargs are treated as config
|
68
|
+
config.update(kwargs)
|
69
|
+
return self._add_node_current(node_type, node_id, config)
|
70
|
+
elif isinstance(args[0], str):
|
71
|
+
# Pattern: add_node("NodeType")
|
72
|
+
return self._add_node_current(args[0], None, {})
|
73
|
+
elif hasattr(args[0], "__name__"):
|
74
|
+
# Pattern: add_node(NodeClass)
|
75
|
+
return self._add_node_alternative(args[0], None, **kwargs)
|
76
|
+
else:
|
77
|
+
from kailash.nodes.base import Node
|
78
|
+
|
79
|
+
if isinstance(args[0], Node):
|
80
|
+
# Pattern: add_node(node_instance)
|
81
|
+
return self._add_node_instance(args[0], None)
|
82
|
+
|
83
|
+
elif len(args) == 3 and isinstance(args[0], str) and isinstance(args[2], dict):
|
84
|
+
# Pattern 1: Current API - add_node("NodeType", "node_id", {"param": value})
|
85
|
+
return self._add_node_current(args[0], args[1], args[2])
|
86
|
+
|
87
|
+
elif len(args) >= 2 and isinstance(args[0], str):
|
88
|
+
# Pattern 2: Legacy fluent API - add_node("node_id", NodeClass, param=value)
|
89
|
+
if hasattr(args[1], "__name__") or isinstance(args[1], type):
|
90
|
+
return self._add_node_legacy_fluent(args[0], args[1], **kwargs)
|
91
|
+
elif isinstance(args[1], str):
|
92
|
+
# Two strings - assume current API: add_node("NodeType", "node_id")
|
93
|
+
config = kwargs if kwargs else (args[2] if len(args) > 2 else {})
|
94
|
+
return self._add_node_current(args[0], args[1], config)
|
95
|
+
else:
|
96
|
+
# Invalid second argument
|
97
|
+
raise WorkflowValidationError(
|
98
|
+
f"Invalid node type: {type(args[1]).__name__}. "
|
99
|
+
"Expected: str (node type name), Node class, or Node instance"
|
100
|
+
)
|
101
|
+
|
102
|
+
elif len(args) >= 2 and hasattr(args[0], "__name__"):
|
103
|
+
# Pattern 3: Alternative - add_node(NodeClass, "node_id", param=value)
|
104
|
+
# Handle both dict config and keyword args
|
105
|
+
if len(args) == 3 and isinstance(args[2], dict):
|
106
|
+
# Config provided as dict
|
107
|
+
return self._add_node_alternative(args[0], args[1], **args[2])
|
108
|
+
else:
|
109
|
+
# Config provided as kwargs
|
110
|
+
return self._add_node_alternative(args[0], args[1], **kwargs)
|
111
|
+
|
112
|
+
elif len(args) >= 2:
|
113
|
+
# Check if first arg is a Node instance
|
114
|
+
from kailash.nodes.base import Node
|
115
|
+
|
116
|
+
if isinstance(args[0], Node):
|
117
|
+
# Pattern 4: Instance - add_node(node_instance, "node_id") or add_node(node_instance, "node_id", config)
|
118
|
+
# Config is ignored for instances
|
119
|
+
return self._add_node_instance(args[0], args[1])
|
120
|
+
elif len(args) == 2:
|
121
|
+
# Invalid arguments for 2-arg call
|
122
|
+
raise WorkflowValidationError(
|
123
|
+
f"Invalid node type: {type(args[0]).__name__}. "
|
124
|
+
"Expected: str (node type name), Node class, or Node instance"
|
125
|
+
)
|
126
|
+
|
127
|
+
# For 3 or more args that don't match other patterns
|
128
|
+
# Error with helpful message
|
129
|
+
raise WorkflowValidationError(
|
130
|
+
f"Invalid add_node signature. Received {len(args)} args: {[type(arg).__name__ for arg in args]}\n"
|
131
|
+
f"Supported patterns:\n"
|
132
|
+
f" add_node('NodeType', 'node_id', {{'param': value}}) # Preferred\n"
|
133
|
+
f" add_node('node_id', NodeClass, param=value) # Legacy\n"
|
134
|
+
f" add_node(NodeClass, 'node_id', param=value) # Alternative\n"
|
135
|
+
f"Examples:\n"
|
136
|
+
f" add_node('HTTPRequestNode', 'api_call', {{'url': 'https://api.com'}})\n"
|
137
|
+
f" add_node('csv_reader', CSVReaderNode, file_path='data.csv')"
|
138
|
+
)
|
139
|
+
|
140
|
+
def _add_node_current(
|
141
|
+
self, node_type: str, node_id: str | None, config: dict[str, Any]
|
142
|
+
) -> str:
|
143
|
+
"""Handle current API pattern: add_node('NodeType', 'node_id', {'param': value})"""
|
144
|
+
return self._add_node_unified(node_type, node_id, config)
|
145
|
+
|
146
|
+
def _add_node_legacy_fluent(
|
147
|
+
self, node_id: str, node_class_or_type: Any, **config
|
148
|
+
) -> "WorkflowBuilder":
|
149
|
+
"""Handle legacy fluent API pattern: add_node('node_id', NodeClass, param=value)"""
|
150
|
+
import warnings
|
151
|
+
|
152
|
+
from kailash.nodes.base import Node
|
153
|
+
|
154
|
+
# If it's a class, validate it's a Node subclass
|
155
|
+
if isinstance(node_class_or_type, type) and not issubclass(
|
156
|
+
node_class_or_type, Node
|
157
|
+
):
|
158
|
+
raise WorkflowValidationError(
|
159
|
+
f"Invalid node type: {node_class_or_type}. Expected a Node subclass or string."
|
160
|
+
)
|
161
|
+
|
162
|
+
warnings.warn(
|
163
|
+
f"Legacy fluent API usage detected. "
|
164
|
+
f"Migration guide:\n"
|
165
|
+
f" OLD: add_node('{node_id}', {getattr(node_class_or_type, '__name__', str(node_class_or_type))}, {list(config.keys())})\n"
|
166
|
+
f" NEW: add_node('{getattr(node_class_or_type, '__name__', str(node_class_or_type))}', '{node_id}', {config})\n"
|
167
|
+
f"Legacy support will be removed in v0.8.0",
|
168
|
+
DeprecationWarning,
|
169
|
+
stacklevel=3,
|
170
|
+
)
|
171
|
+
|
172
|
+
if hasattr(node_class_or_type, "__name__"):
|
173
|
+
node_type = node_class_or_type.__name__
|
174
|
+
else:
|
175
|
+
node_type = str(node_class_or_type)
|
176
|
+
|
177
|
+
self._add_node_unified(node_type, node_id, config)
|
178
|
+
return self # Return self for fluent chaining
|
179
|
+
|
180
|
+
def _add_node_alternative(
|
181
|
+
self, node_class: type, node_id: str | None, **config
|
182
|
+
) -> str:
|
183
|
+
"""Handle alternative pattern: add_node(NodeClass, 'node_id', param=value)"""
|
184
|
+
import warnings
|
185
|
+
|
186
|
+
from kailash.nodes.base import Node
|
187
|
+
|
188
|
+
# Validate that node_class is actually a Node subclass
|
189
|
+
if not isinstance(node_class, type) or not issubclass(node_class, Node):
|
190
|
+
raise WorkflowValidationError(
|
191
|
+
f"Invalid node type: {node_class}. Expected a Node subclass."
|
192
|
+
)
|
193
|
+
|
194
|
+
# Generate ID if not provided
|
195
|
+
if node_id is None:
|
196
|
+
node_id = f"node_{uuid.uuid4().hex[:8]}"
|
197
|
+
|
198
|
+
warnings.warn(
|
199
|
+
f"Alternative API usage detected. Consider using preferred pattern:\n"
|
200
|
+
f" CURRENT: add_node({node_class.__name__}, '{node_id}', {list(config.keys())})\n"
|
201
|
+
f" PREFERRED: add_node('{node_class.__name__}', '{node_id}', {config})",
|
202
|
+
UserWarning,
|
203
|
+
stacklevel=3,
|
204
|
+
)
|
205
|
+
|
206
|
+
# Store the class reference along with the type name
|
207
|
+
self.nodes[node_id] = {
|
208
|
+
"type": node_class.__name__,
|
209
|
+
"config": config,
|
210
|
+
"class": node_class,
|
211
|
+
}
|
212
|
+
logger.info(f"Added node '{node_id}' of type '{node_class.__name__}'")
|
213
|
+
return node_id
|
214
|
+
|
215
|
+
def _add_node_instance(self, node_instance: "Node", node_id: str | None) -> str:
|
216
|
+
"""Handle instance pattern: add_node(node_instance, 'node_id')"""
|
217
|
+
import warnings
|
218
|
+
|
219
|
+
# Generate ID if not provided
|
220
|
+
if node_id is None:
|
221
|
+
node_id = f"node_{uuid.uuid4().hex[:8]}"
|
222
|
+
|
223
|
+
warnings.warn(
|
224
|
+
f"Instance-based API usage detected. Consider using preferred pattern:\n"
|
225
|
+
f" CURRENT: add_node(<instance>, '{node_id}')\n"
|
226
|
+
f" PREFERRED: add_node('{node_instance.__class__.__name__}', '{node_id}', {{'param': value}})",
|
227
|
+
UserWarning,
|
228
|
+
stacklevel=3,
|
229
|
+
)
|
230
|
+
|
231
|
+
# Store the instance
|
232
|
+
self.nodes[node_id] = {
|
233
|
+
"instance": node_instance,
|
234
|
+
"type": node_instance.__class__.__name__,
|
235
|
+
}
|
236
|
+
logger.info(
|
237
|
+
f"Added node '{node_id}' with instance of type '{node_instance.__class__.__name__}'"
|
238
|
+
)
|
239
|
+
return node_id
|
240
|
+
|
241
|
+
def _add_node_unified(
|
23
242
|
self,
|
24
|
-
node_type: str
|
243
|
+
node_type: str,
|
25
244
|
node_id: str | None = None,
|
26
245
|
config: dict[str, Any] | None = None,
|
27
246
|
) -> str:
|
28
247
|
"""
|
29
|
-
|
248
|
+
Unified implementation for all add_node patterns.
|
30
249
|
|
31
250
|
Args:
|
32
|
-
node_type: Node type name (string)
|
251
|
+
node_type: Node type name (string)
|
33
252
|
node_id: Unique identifier for this node (auto-generated if not provided)
|
34
|
-
config: Configuration for the node
|
253
|
+
config: Configuration for the node
|
35
254
|
|
36
255
|
Returns:
|
37
|
-
Node ID
|
256
|
+
Node ID
|
38
257
|
|
39
258
|
Raises:
|
40
259
|
WorkflowValidationError: If node_id is already used
|
@@ -80,6 +299,39 @@ class WorkflowBuilder:
|
|
80
299
|
logger.info(f"Added node '{node_id}' of type '{type_name}'")
|
81
300
|
return node_id
|
82
301
|
|
302
|
+
# Fluent API methods for backward compatibility
|
303
|
+
def add_node_fluent(
|
304
|
+
self, node_id: str, node_class_or_type: Any, **config
|
305
|
+
) -> "WorkflowBuilder":
|
306
|
+
"""
|
307
|
+
DEPRECATED: Fluent API for backward compatibility.
|
308
|
+
Use add_node(node_type, node_id, config) instead.
|
309
|
+
|
310
|
+
Args:
|
311
|
+
node_id: Node identifier
|
312
|
+
node_class_or_type: Node class or type
|
313
|
+
**config: Node configuration as keyword arguments
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
Self for method chaining
|
317
|
+
"""
|
318
|
+
import warnings
|
319
|
+
|
320
|
+
warnings.warn(
|
321
|
+
"Fluent API is deprecated. Use add_node(node_type, node_id, config) instead.",
|
322
|
+
DeprecationWarning,
|
323
|
+
stacklevel=2,
|
324
|
+
)
|
325
|
+
|
326
|
+
if hasattr(node_class_or_type, "__name__"):
|
327
|
+
# Node class
|
328
|
+
self.add_node(node_class_or_type.__name__, node_id, config)
|
329
|
+
else:
|
330
|
+
# Assume string type
|
331
|
+
self.add_node(str(node_class_or_type), node_id, config)
|
332
|
+
|
333
|
+
return self
|
334
|
+
|
83
335
|
def add_node_instance(self, node_instance: Any, node_id: str | None = None) -> str:
|
84
336
|
"""
|
85
337
|
Add a node instance to the workflow.
|
@@ -124,7 +376,7 @@ class WorkflowBuilder:
|
|
124
376
|
|
125
377
|
def add_connection(
|
126
378
|
self, from_node: str, from_output: str, to_node: str, to_input: str
|
127
|
-
) ->
|
379
|
+
) -> "WorkflowBuilder":
|
128
380
|
"""
|
129
381
|
Connect two nodes in the workflow.
|
130
382
|
|
@@ -161,6 +413,7 @@ class WorkflowBuilder:
|
|
161
413
|
self.connections.append(connection)
|
162
414
|
|
163
415
|
logger.info(f"Connected '{from_node}.{from_output}' to '{to_node}.{to_input}'")
|
416
|
+
return self
|
164
417
|
|
165
418
|
def connect(
|
166
419
|
self,
|
@@ -399,9 +652,21 @@ class WorkflowBuilder:
|
|
399
652
|
# Dict format: {node_id: {type: "...", parameters: {...}}}
|
400
653
|
for node_id, node_config in nodes_config.items():
|
401
654
|
node_type = node_config.get("type")
|
402
|
-
|
403
|
-
|
404
|
-
|
655
|
+
|
656
|
+
# Handle parameter naming inconsistencies - prefer 'parameters' over 'config'
|
657
|
+
if "parameters" in node_config:
|
658
|
+
node_params = node_config["parameters"]
|
659
|
+
elif "config" in node_config:
|
660
|
+
node_params = node_config["config"]
|
661
|
+
else:
|
662
|
+
node_params = {}
|
663
|
+
|
664
|
+
# Ensure node_params is a dictionary
|
665
|
+
if not isinstance(node_params, dict):
|
666
|
+
logger.warning(
|
667
|
+
f"Node '{node_id}' parameters must be a dict, got {type(node_params)}. Using empty dict."
|
668
|
+
)
|
669
|
+
node_params = {}
|
405
670
|
|
406
671
|
if not node_type:
|
407
672
|
raise WorkflowValidationError(
|
@@ -414,7 +679,21 @@ class WorkflowBuilder:
|
|
414
679
|
for node_config in nodes_config:
|
415
680
|
node_id = node_config.get("id")
|
416
681
|
node_type = node_config.get("type")
|
417
|
-
|
682
|
+
|
683
|
+
# Handle parameter naming inconsistencies - prefer 'parameters' over 'config'
|
684
|
+
if "parameters" in node_config:
|
685
|
+
node_params = node_config["parameters"]
|
686
|
+
elif "config" in node_config:
|
687
|
+
node_params = node_config["config"]
|
688
|
+
else:
|
689
|
+
node_params = {}
|
690
|
+
|
691
|
+
# Ensure node_params is a dictionary
|
692
|
+
if not isinstance(node_params, dict):
|
693
|
+
logger.warning(
|
694
|
+
f"Node '{node_id}' parameters must be a dict, got {type(node_params)}. Using empty dict."
|
695
|
+
)
|
696
|
+
node_params = {}
|
418
697
|
|
419
698
|
if not node_id:
|
420
699
|
raise WorkflowValidationError("Node ID is required")
|
kailash/workflow/validation.py
CHANGED
@@ -390,6 +390,49 @@ class CycleLinter:
|
|
390
390
|
)
|
391
391
|
)
|
392
392
|
|
393
|
+
# Check for potentially problematic mappings
|
394
|
+
if source_param in [
|
395
|
+
"result",
|
396
|
+
"output",
|
397
|
+
"data",
|
398
|
+
] and target_param in ["result", "output", "data"]:
|
399
|
+
if source_param != target_param:
|
400
|
+
self.issues.append(
|
401
|
+
ValidationIssue(
|
402
|
+
severity=IssueSeverity.INFO,
|
403
|
+
category="parameter_mapping",
|
404
|
+
code="CYC010A",
|
405
|
+
message=f"Generic parameter mapping '{source_param}' -> '{target_param}' in cycle {cycle_id}",
|
406
|
+
cycle_id=cycle_id,
|
407
|
+
suggestion="Consider using more specific parameter names for clarity",
|
408
|
+
documentation_link="guide/mistakes/063-cyclic-parameter-propagation-multi-fix.md",
|
409
|
+
)
|
410
|
+
)
|
411
|
+
|
412
|
+
# Check for dot notation in mappings
|
413
|
+
if (
|
414
|
+
"." in source_param
|
415
|
+
and target_param == source_param.split(".")[-1]
|
416
|
+
):
|
417
|
+
# This is actually a good pattern - dot notation to specific field
|
418
|
+
pass
|
419
|
+
elif "." not in source_param and "." not in target_param:
|
420
|
+
# Simple mapping - check if it makes sense
|
421
|
+
if source_param.startswith(
|
422
|
+
"temp_"
|
423
|
+
) or target_param.startswith("temp_"):
|
424
|
+
self.issues.append(
|
425
|
+
ValidationIssue(
|
426
|
+
severity=IssueSeverity.INFO,
|
427
|
+
category="parameter_mapping",
|
428
|
+
code="CYC010B",
|
429
|
+
message=f"Temporary parameter mapping '{source_param}' -> '{target_param}' in cycle {cycle_id}",
|
430
|
+
cycle_id=cycle_id,
|
431
|
+
suggestion="Consider using permanent parameter names for production workflows",
|
432
|
+
documentation_link="guide/mistakes/063-cyclic-parameter-propagation-multi-fix.md",
|
433
|
+
)
|
434
|
+
)
|
435
|
+
|
393
436
|
# Check for missing parameter propagation
|
394
437
|
if not mapping and len(cycle_nodes) > 1:
|
395
438
|
self.issues.append(
|
@@ -589,20 +632,53 @@ class CycleLinter:
|
|
589
632
|
|
590
633
|
def _has_unsafe_parameter_access(self, code: str) -> bool:
|
591
634
|
"""Check if PythonCodeNode has unsafe parameter access."""
|
592
|
-
|
635
|
+
import re
|
636
|
+
|
637
|
+
# Look for direct parameter access without try/except or safety checks
|
593
638
|
lines = code.split("\n")
|
594
639
|
|
640
|
+
# Common parameter names that might be unsafe
|
641
|
+
unsafe_patterns = [
|
642
|
+
r"\b(data|input|params|context|kwargs|args)\[", # Direct indexing
|
643
|
+
r"\b(data|input|params|context|kwargs|args)\.", # Direct attribute access
|
644
|
+
r"\b(data|input|params|context|kwargs|args)\.get\(", # .get() without default
|
645
|
+
]
|
646
|
+
|
647
|
+
# Safety patterns that indicate safe access
|
648
|
+
safety_patterns = [
|
649
|
+
r"try\s*:",
|
650
|
+
r"except\s*:",
|
651
|
+
r"if\s+.*\s+is\s+not\s+None\s*:",
|
652
|
+
r"if\s+.*\s+in\s+",
|
653
|
+
r"\.get\(.*,.*\)", # .get() with default value
|
654
|
+
r"isinstance\s*\(",
|
655
|
+
r"hasattr\s*\(",
|
656
|
+
]
|
657
|
+
|
658
|
+
has_unsafe_access = False
|
659
|
+
has_safety_checks = False
|
660
|
+
|
595
661
|
for line in lines:
|
596
662
|
line = line.strip()
|
597
663
|
if line and not line.startswith("#"):
|
598
|
-
# Check for
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
return True
|
664
|
+
# Check for unsafe patterns
|
665
|
+
for pattern in unsafe_patterns:
|
666
|
+
if re.search(pattern, line):
|
667
|
+
has_unsafe_access = True
|
668
|
+
break
|
604
669
|
|
605
|
-
|
670
|
+
# Check for safety patterns
|
671
|
+
for pattern in safety_patterns:
|
672
|
+
if re.search(pattern, line):
|
673
|
+
has_safety_checks = True
|
674
|
+
break
|
675
|
+
|
676
|
+
# Also check for undefined variables (potential parameters)
|
677
|
+
undefined_vars = self._find_undefined_variables(code)
|
678
|
+
if undefined_vars:
|
679
|
+
has_unsafe_access = True
|
680
|
+
|
681
|
+
return has_unsafe_access and not has_safety_checks
|
606
682
|
|
607
683
|
def _is_defined_before_use(self, var_name: str, code: str) -> bool:
|
608
684
|
"""Check if variable is defined before use in code."""
|
@@ -619,6 +695,66 @@ class CycleLinter:
|
|
619
695
|
|
620
696
|
return True
|
621
697
|
|
698
|
+
def _find_undefined_variables(self, code: str) -> list[str]:
|
699
|
+
"""Find variables that are used but not defined in the code."""
|
700
|
+
import re
|
701
|
+
|
702
|
+
lines = code.split("\n")
|
703
|
+
defined_vars = set()
|
704
|
+
used_vars = set()
|
705
|
+
|
706
|
+
# Built-in variables and functions that don't need definition
|
707
|
+
builtin_vars = {
|
708
|
+
"len",
|
709
|
+
"sum",
|
710
|
+
"min",
|
711
|
+
"max",
|
712
|
+
"dict",
|
713
|
+
"list",
|
714
|
+
"set",
|
715
|
+
"str",
|
716
|
+
"int",
|
717
|
+
"float",
|
718
|
+
"bool",
|
719
|
+
"sorted",
|
720
|
+
"print",
|
721
|
+
"isinstance",
|
722
|
+
"type",
|
723
|
+
"hasattr",
|
724
|
+
"getattr",
|
725
|
+
"True",
|
726
|
+
"False",
|
727
|
+
"None",
|
728
|
+
"range",
|
729
|
+
"enumerate",
|
730
|
+
"zip",
|
731
|
+
"any",
|
732
|
+
"all",
|
733
|
+
}
|
734
|
+
|
735
|
+
for line in lines:
|
736
|
+
line = line.strip()
|
737
|
+
if line and not line.startswith("#"):
|
738
|
+
# Find variable definitions
|
739
|
+
if (
|
740
|
+
"=" in line
|
741
|
+
and not line.startswith("if")
|
742
|
+
and not line.startswith("elif")
|
743
|
+
):
|
744
|
+
var_match = re.match(r"^([a-zA-Z_]\w*)\s*=", line)
|
745
|
+
if var_match:
|
746
|
+
defined_vars.add(var_match.group(1))
|
747
|
+
|
748
|
+
# Find variable uses
|
749
|
+
variables = re.findall(r"\b([a-zA-Z_]\w*)\b", line)
|
750
|
+
for var in variables:
|
751
|
+
if var not in builtin_vars and not var.startswith("_"):
|
752
|
+
used_vars.add(var)
|
753
|
+
|
754
|
+
# Return variables that are used but not defined
|
755
|
+
undefined = used_vars - defined_vars
|
756
|
+
return list(undefined)
|
757
|
+
|
622
758
|
def _is_valid_condition_syntax(self, condition: str) -> bool:
|
623
759
|
"""Check if convergence condition has valid Python syntax."""
|
624
760
|
try:
|