tasktree 0.0.20__py3-none-any.whl → 0.0.21__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.
tasktree/parser.py CHANGED
@@ -1,4 +1,7 @@
1
- """Parse recipe YAML files and handle imports."""
1
+ """
2
+ Parse recipe YAML files and handle imports.
3
+ @athena: ec0087b16df9
4
+ """
2
5
 
3
6
  from __future__ import annotations
4
7
 
@@ -16,17 +19,22 @@ from tasktree.types import get_click_type
16
19
 
17
20
 
18
21
  class CircularImportError(Exception):
19
- """Raised when a circular import is detected."""
22
+ """
23
+ Raised when a circular import is detected.
24
+ @athena: 935d53bc7d05
25
+ """
20
26
  pass
21
27
 
22
28
 
23
29
  @dataclass
24
30
  class Environment:
25
- """Represents an execution environment configuration.
31
+ """
32
+ Represents an execution environment configuration.
26
33
 
27
34
  Can be either a shell environment or a Docker environment:
28
35
  - Shell environment: has 'shell' field, executes directly on host
29
36
  - Docker environment: has 'dockerfile' field, executes in container
37
+ @athena: cfe3f8754968
30
38
  """
31
39
 
32
40
  name: str
@@ -44,14 +52,20 @@ class Environment:
44
52
  run_as_root: bool = False # If True, skip user mapping (run as root in container)
45
53
 
46
54
  def __post_init__(self):
47
- """Ensure args is in the correct format."""
55
+ """
56
+ Ensure args is in the correct format.
57
+ @athena: a4292f3f4150
58
+ """
48
59
  if isinstance(self.args, str):
49
60
  self.args = [self.args]
50
61
 
51
62
 
52
63
  @dataclass
53
64
  class Task:
54
- """Represents a task definition."""
65
+ """
66
+ Represents a task definition.
67
+ @athena: f516b5ae61c5
68
+ """
55
69
 
56
70
  name: str
57
71
  cmd: str
@@ -73,8 +87,15 @@ class Task:
73
87
  _input_map: dict[str, str] = field(init=False, default_factory=dict, repr=False) # name → path mapping
74
88
  _anonymous_inputs: list[str] = field(init=False, default_factory=list, repr=False) # unnamed inputs
75
89
 
90
+ # Internal fields for positional input/output access (built in __post_init__)
91
+ _indexed_inputs: list[str] = field(init=False, default_factory=list, repr=False) # all inputs in YAML order
92
+ _indexed_outputs: list[str] = field(init=False, default_factory=list, repr=False) # all outputs in YAML order
93
+
76
94
  def __post_init__(self):
77
- """Ensure lists are always lists and build output maps."""
95
+ """
96
+ Ensure lists are always lists and build input/output maps and indexed lists.
97
+ @athena: a48b1eba81cd
98
+ """
78
99
  if isinstance(self.deps, str):
79
100
  self.deps = [self.deps]
80
101
  if isinstance(self.inputs, str):
@@ -100,6 +121,7 @@ class Task:
100
121
  # Build output maps for efficient lookup
101
122
  self._output_map = {}
102
123
  self._anonymous_outputs = []
124
+ self._indexed_outputs = []
103
125
 
104
126
  for idx, output in enumerate(self.outputs):
105
127
  if isinstance(output, dict):
@@ -128,9 +150,11 @@ class Task:
128
150
  )
129
151
 
130
152
  self._output_map[name] = path
153
+ self._indexed_outputs.append(path)
131
154
  elif isinstance(output, str):
132
155
  # Anonymous output: just store
133
156
  self._anonymous_outputs.append(output)
157
+ self._indexed_outputs.append(output)
134
158
  else:
135
159
  raise ValueError(
136
160
  f"Task '{self.name}': Output at index {idx} must be a string or dict, got {type(output).__name__}: {output}"
@@ -139,6 +163,7 @@ class Task:
139
163
  # Build input maps for efficient lookup
140
164
  self._input_map = {}
141
165
  self._anonymous_inputs = []
166
+ self._indexed_inputs = []
142
167
 
143
168
  for idx, input_item in enumerate(self.inputs):
144
169
  if isinstance(input_item, dict):
@@ -167,9 +192,11 @@ class Task:
167
192
  )
168
193
 
169
194
  self._input_map[name] = path
195
+ self._indexed_inputs.append(path)
170
196
  elif isinstance(input_item, str):
171
197
  # Anonymous input: just store
172
198
  self._anonymous_inputs.append(input_item)
199
+ self._indexed_inputs.append(input_item)
173
200
  else:
174
201
  raise ValueError(
175
202
  f"Task '{self.name}': Input at index {idx} must be a string or dict, got {type(input_item).__name__}: {input_item}"
@@ -178,23 +205,28 @@ class Task:
178
205
 
179
206
  @dataclass
180
207
  class DependencySpec:
181
- """Parsed dependency specification with potential template placeholders.
208
+ """
209
+ Parsed dependency specification with potential template placeholders.
182
210
 
183
211
  This represents a dependency as defined in the recipe file, before template
184
212
  substitution. Argument values may contain {{ arg.* }} templates that will be
185
213
  substituted with parent task's argument values during graph construction.
186
214
 
187
215
  Attributes:
188
- task_name: Name of the dependency task
189
- arg_templates: Dictionary mapping argument names to string templates
190
- (None if no args specified). All values are strings, even
191
- for numeric types, to preserve template placeholders.
216
+ task_name: Name of the dependency task
217
+ arg_templates: Dictionary mapping argument names to string templates
218
+ (None if no args specified). All values are strings, even
219
+ for numeric types, to preserve template placeholders.
220
+ @athena: 7b2f8a15d312
192
221
  """
193
222
  task_name: str
194
223
  arg_templates: dict[str, str] | None = None
195
224
 
196
225
  def __str__(self) -> str:
197
- """String representation for display."""
226
+ """
227
+ String representation for display.
228
+ @athena: e5669be6329b
229
+ """
198
230
  if not self.arg_templates:
199
231
  return self.task_name
200
232
  args_str = ", ".join(f"{k}={v}" for k, v in self.arg_templates.items())
@@ -203,17 +235,22 @@ class DependencySpec:
203
235
 
204
236
  @dataclass
205
237
  class DependencyInvocation:
206
- """Represents a task dependency invocation with optional arguments.
238
+ """
239
+ Represents a task dependency invocation with optional arguments.
207
240
 
208
241
  Attributes:
209
- task_name: Name of the dependency task
210
- args: Dictionary of argument names to values (None if no args specified)
242
+ task_name: Name of the dependency task
243
+ args: Dictionary of argument names to values (None if no args specified)
244
+ @athena: 0c023366160b
211
245
  """
212
246
  task_name: str
213
247
  args: dict[str, Any] | None = None
214
248
 
215
249
  def __str__(self) -> str:
216
- """String representation for display."""
250
+ """
251
+ String representation for display.
252
+ @athena: 22fc0502192b
253
+ """
217
254
  if not self.args:
218
255
  return self.task_name
219
256
  args_str = ", ".join(f"{k}={v}" for k, v in self.args.items())
@@ -222,16 +259,18 @@ class DependencyInvocation:
222
259
 
223
260
  @dataclass
224
261
  class ArgSpec:
225
- """Represents a parsed argument specification.
262
+ """
263
+ Represents a parsed argument specification.
226
264
 
227
265
  Attributes:
228
- name: Argument name
229
- arg_type: Type of the argument (str, int, float, bool, path)
230
- default: Default value as a string (None if no default)
231
- is_exported: Whether the argument is exported as an environment variable
232
- min_val: Minimum value for numeric arguments (None if not specified)
233
- max_val: Maximum value for numeric arguments (None if not specified)
234
- choices: List of valid choices for the argument (None if not specified)
266
+ name: Argument name
267
+ arg_type: Type of the argument (str, int, float, bool, path)
268
+ default: Default value as a string (None if no default)
269
+ is_exported: Whether the argument is exported as an environment variable
270
+ min_val: Minimum value for numeric arguments (None if not specified)
271
+ max_val: Maximum value for numeric arguments (None if not specified)
272
+ choices: List of valid choices for the argument (None if not specified)
273
+ @athena: fcaf20fb1ca2
235
274
  """
236
275
  name: str
237
276
  arg_type: str
@@ -244,7 +283,10 @@ class ArgSpec:
244
283
 
245
284
  @dataclass
246
285
  class Recipe:
247
- """Represents a parsed recipe file with all tasks."""
286
+ """
287
+ Represents a parsed recipe file with all tasks.
288
+ @athena: 47f568c77013
289
+ """
248
290
 
249
291
  tasks: dict[str, Task]
250
292
  project_root: Path
@@ -259,33 +301,41 @@ class Recipe:
259
301
  _original_yaml_data: dict[str, Any] = field(default_factory=dict) # Store original YAML data for lazy evaluation context
260
302
 
261
303
  def get_task(self, name: str) -> Task | None:
262
- """Get task by name.
304
+ """
305
+ Get task by name.
263
306
 
264
307
  Args:
265
- name: Task name (may be namespaced like 'build.compile')
308
+ name: Task name (may be namespaced like 'build.compile')
266
309
 
267
310
  Returns:
268
- Task if found, None otherwise
311
+ Task if found, None otherwise
312
+ @athena: 3f8137d71757
269
313
  """
270
314
  return self.tasks.get(name)
271
315
 
272
316
  def task_names(self) -> list[str]:
273
- """Get all task names."""
317
+ """
318
+ Get all task names.
319
+ @athena: 1df54563a7b6
320
+ """
274
321
  return list(self.tasks.keys())
275
322
 
276
323
  def get_environment(self, name: str) -> Environment | None:
277
- """Get environment by name.
324
+ """
325
+ Get environment by name.
278
326
 
279
327
  Args:
280
- name: Environment name
328
+ name: Environment name
281
329
 
282
330
  Returns:
283
- Environment if found, None otherwise
331
+ Environment if found, None otherwise
332
+ @athena: 098227ca38a2
284
333
  """
285
334
  return self.environments.get(name)
286
335
 
287
336
  def evaluate_variables(self, root_task: str | None = None) -> None:
288
- """Evaluate variables lazily based on task reachability.
337
+ """
338
+ Evaluate variables lazily based on task reachability.
289
339
 
290
340
  This method implements lazy variable evaluation, which only evaluates
291
341
  variables that are actually reachable from the target task. This provides:
@@ -299,15 +349,16 @@ class Recipe:
299
349
  This method is idempotent - calling it multiple times is safe (uses caching).
300
350
 
301
351
  Args:
302
- root_task: Optional task name to determine reachability (None = evaluate all)
352
+ root_task: Optional task name to determine reachability (None = evaluate all)
303
353
 
304
354
  Raises:
305
- ValueError: If variable evaluation or substitution fails
355
+ ValueError: If variable evaluation or substitution fails
306
356
 
307
357
  Example:
308
- >>> recipe = parse_recipe(path) # Variables not yet evaluated
309
- >>> recipe.evaluate_variables("build") # Evaluate only reachable variables
310
- >>> # Now recipe.evaluated_variables contains only vars used by "build" task
358
+ >>> recipe = parse_recipe(path) # Variables not yet evaluated
359
+ >>> recipe.evaluate_variables("build") # Evaluate only reachable variables
360
+ >>> # Now recipe.evaluated_variables contains only vars used by "build" task
361
+ @athena: d8de7b5f42b6
311
362
  """
312
363
  if self._variables_evaluated:
313
364
  return # Already evaluated, skip (idempotent)
@@ -454,7 +505,8 @@ class Recipe:
454
505
 
455
506
 
456
507
  def find_recipe_file(start_dir: Path | None = None) -> Path | None:
457
- """Find recipe file in current or parent directories.
508
+ """
509
+ Find recipe file in current or parent directories.
458
510
 
459
511
  Looks for recipe files matching these patterns (in order of preference):
460
512
  - tasktree.yaml
@@ -466,13 +518,14 @@ def find_recipe_file(start_dir: Path | None = None) -> Path | None:
466
518
  with instructions to use --tasks option.
467
519
 
468
520
  Args:
469
- start_dir: Directory to start searching from (defaults to cwd)
521
+ start_dir: Directory to start searching from (defaults to cwd)
470
522
 
471
523
  Returns:
472
- Path to recipe file if found, None otherwise
524
+ Path to recipe file if found, None otherwise
473
525
 
474
526
  Raises:
475
- ValueError: If multiple recipe files found in the same directory
527
+ ValueError: If multiple recipe files found in the same directory
528
+ @athena: 38ccb0c1bb86
476
529
  """
477
530
  if start_dir is None:
478
531
  start_dir = Path.cwd()
@@ -532,13 +585,15 @@ def find_recipe_file(start_dir: Path | None = None) -> Path | None:
532
585
 
533
586
 
534
587
  def _validate_variable_name(name: str) -> None:
535
- """Validate that a variable name is a valid identifier.
588
+ """
589
+ Validate that a variable name is a valid identifier.
536
590
 
537
591
  Args:
538
- name: Variable name to validate
592
+ name: Variable name to validate
539
593
 
540
594
  Raises:
541
- ValueError: If name is not a valid identifier
595
+ ValueError: If name is not a valid identifier
596
+ @athena: 61f92f7ad278
542
597
  """
543
598
  if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name):
544
599
  raise ValueError(
@@ -548,16 +603,18 @@ def _validate_variable_name(name: str) -> None:
548
603
 
549
604
 
550
605
  def _infer_variable_type(value: Any) -> str:
551
- """Infer type name from Python value.
606
+ """
607
+ Infer type name from Python value.
552
608
 
553
609
  Args:
554
- value: Python value from YAML
610
+ value: Python value from YAML
555
611
 
556
612
  Returns:
557
- Type name string (str, int, float, bool)
613
+ Type name string (str, int, float, bool)
558
614
 
559
615
  Raises:
560
- ValueError: If value type is not supported
616
+ ValueError: If value type is not supported
617
+ @athena: 335ae24e1504
561
618
  """
562
619
  type_map = {
563
620
  str: "str",
@@ -575,29 +632,33 @@ def _infer_variable_type(value: Any) -> str:
575
632
 
576
633
 
577
634
  def _is_env_variable_reference(value: Any) -> bool:
578
- """Check if value is an environment variable reference.
635
+ """
636
+ Check if value is an environment variable reference.
579
637
 
580
638
  Args:
581
- value: Raw value from YAML
639
+ value: Raw value from YAML
582
640
 
583
641
  Returns:
584
- True if value is { env: VAR_NAME } dict
642
+ True if value is { env: VAR_NAME } dict
643
+ @athena: c01927ec19ef
585
644
  """
586
645
  return isinstance(value, dict) and "env" in value
587
646
 
588
647
 
589
648
  def _validate_env_variable_reference(var_name: str, value: dict) -> tuple[str, str | None]:
590
- """Validate and extract environment variable name and optional default from reference.
649
+ """
650
+ Validate and extract environment variable name and optional default from reference.
591
651
 
592
652
  Args:
593
- var_name: Name of the variable being defined
594
- value: Dict that should be { env: ENV_VAR_NAME } or { env: ENV_VAR_NAME, default: "value" }
653
+ var_name: Name of the variable being defined
654
+ value: Dict that should be { env: ENV_VAR_NAME } or { env: ENV_VAR_NAME, default: "value" }
595
655
 
596
656
  Returns:
597
- Tuple of (environment variable name, default value or None)
657
+ Tuple of (environment variable name, default value or None)
598
658
 
599
659
  Raises:
600
- ValueError: If reference is invalid
660
+ ValueError: If reference is invalid
661
+ @athena: 9fc8b2333b54
601
662
  """
602
663
  # Validate dict structure - allow 'env' and optionally 'default'
603
664
  valid_keys = {"env", "default"}
@@ -651,18 +712,20 @@ def _validate_env_variable_reference(var_name: str, value: dict) -> tuple[str, s
651
712
 
652
713
 
653
714
  def _resolve_env_variable(var_name: str, env_var_name: str, default: str | None = None) -> str:
654
- """Resolve environment variable value.
715
+ """
716
+ Resolve environment variable value.
655
717
 
656
718
  Args:
657
- var_name: Name of the variable being defined
658
- env_var_name: Name of environment variable to read
659
- default: Optional default value to use if environment variable is not set
719
+ var_name: Name of the variable being defined
720
+ env_var_name: Name of environment variable to read
721
+ default: Optional default value to use if environment variable is not set
660
722
 
661
723
  Returns:
662
- Environment variable value as string, or default if not set and default provided
724
+ Environment variable value as string, or default if not set and default provided
663
725
 
664
726
  Raises:
665
- ValueError: If environment variable is not set and no default provided
727
+ ValueError: If environment variable is not set and no default provided
728
+ @athena: c00d3a241a99
666
729
  """
667
730
  value = os.environ.get(env_var_name, default)
668
731
 
@@ -680,29 +743,33 @@ def _resolve_env_variable(var_name: str, env_var_name: str, default: str | None
680
743
 
681
744
 
682
745
  def _is_file_read_reference(value: Any) -> bool:
683
- """Check if value is a file read reference.
746
+ """
747
+ Check if value is a file read reference.
684
748
 
685
749
  Args:
686
- value: Raw value from YAML
750
+ value: Raw value from YAML
687
751
 
688
752
  Returns:
689
- True if value is { read: filepath } dict
753
+ True if value is { read: filepath } dict
754
+ @athena: da129db1b17b
690
755
  """
691
756
  return isinstance(value, dict) and "read" in value
692
757
 
693
758
 
694
759
  def _validate_file_read_reference(var_name: str, value: dict) -> str:
695
- """Validate and extract filepath from file read reference.
760
+ """
761
+ Validate and extract filepath from file read reference.
696
762
 
697
763
  Args:
698
- var_name: Name of the variable being defined
699
- value: Dict that should be { read: filepath }
764
+ var_name: Name of the variable being defined
765
+ value: Dict that should be { read: filepath }
700
766
 
701
767
  Returns:
702
- Filepath string
768
+ Filepath string
703
769
 
704
770
  Raises:
705
- ValueError: If reference is invalid
771
+ ValueError: If reference is invalid
772
+ @athena: 2615951372fc
706
773
  """
707
774
  # Validate dict structure (only "read" key allowed)
708
775
  if len(value) != 1:
@@ -728,7 +795,8 @@ def _validate_file_read_reference(var_name: str, value: dict) -> str:
728
795
 
729
796
 
730
797
  def _resolve_file_path(filepath: str, recipe_file_path: Path) -> Path:
731
- """Resolve file path relative to recipe file location.
798
+ """
799
+ Resolve file path relative to recipe file location.
732
800
 
733
801
  Handles three path types:
734
802
  1. Tilde paths (~): Expand to user home directory
@@ -736,11 +804,12 @@ def _resolve_file_path(filepath: str, recipe_file_path: Path) -> Path:
736
804
  3. Relative paths: Resolve relative to recipe file's directory
737
805
 
738
806
  Args:
739
- filepath: Path string from YAML (may be relative, absolute, or tilde)
740
- recipe_file_path: Path to the recipe file containing the variable
807
+ filepath: Path string from YAML (may be relative, absolute, or tilde)
808
+ recipe_file_path: Path to the recipe file containing the variable
741
809
 
742
810
  Returns:
743
- Resolved absolute Path object
811
+ Resolved absolute Path object
812
+ @athena: e80470e9c7d6
744
813
  """
745
814
  # Expand tilde to home directory
746
815
  if filepath.startswith("~"):
@@ -758,18 +827,20 @@ def _resolve_file_path(filepath: str, recipe_file_path: Path) -> Path:
758
827
 
759
828
 
760
829
  def _resolve_file_variable(var_name: str, filepath: str, resolved_path: Path) -> str:
761
- """Read file contents for variable value.
830
+ """
831
+ Read file contents for variable value.
762
832
 
763
833
  Args:
764
- var_name: Name of the variable being defined
765
- filepath: Original filepath string (for error messages)
766
- resolved_path: Resolved absolute path to the file
834
+ var_name: Name of the variable being defined
835
+ filepath: Original filepath string (for error messages)
836
+ resolved_path: Resolved absolute path to the file
767
837
 
768
838
  Returns:
769
- File contents as string (with trailing newline stripped)
839
+ File contents as string (with trailing newline stripped)
770
840
 
771
841
  Raises:
772
- ValueError: If file doesn't exist, can't be read, or contains invalid UTF-8
842
+ ValueError: If file doesn't exist, can't be read, or contains invalid UTF-8
843
+ @athena: cab84337f145
773
844
  """
774
845
  # Check file exists
775
846
  if not resolved_path.exists():
@@ -811,29 +882,33 @@ def _resolve_file_variable(var_name: str, filepath: str, resolved_path: Path) ->
811
882
 
812
883
 
813
884
  def _is_eval_reference(value: Any) -> bool:
814
- """Check if value is an eval command reference.
885
+ """
886
+ Check if value is an eval command reference.
815
887
 
816
888
  Args:
817
- value: Raw value from YAML
889
+ value: Raw value from YAML
818
890
 
819
891
  Returns:
820
- True if value is { eval: command } dict
892
+ True if value is { eval: command } dict
893
+ @athena: 121784f6d4ab
821
894
  """
822
895
  return isinstance(value, dict) and "eval" in value
823
896
 
824
897
 
825
898
  def _validate_eval_reference(var_name: str, value: dict) -> str:
826
- """Validate and extract command from eval reference.
899
+ """
900
+ Validate and extract command from eval reference.
827
901
 
828
902
  Args:
829
- var_name: Name of the variable being defined
830
- value: Dict that should be { eval: command }
903
+ var_name: Name of the variable being defined
904
+ value: Dict that should be { eval: command }
831
905
 
832
906
  Returns:
833
- Command string
907
+ Command string
834
908
 
835
909
  Raises:
836
- ValueError: If reference is invalid
910
+ ValueError: If reference is invalid
911
+ @athena: f3cde1011d2d
837
912
  """
838
913
  # Validate dict structure (only "eval" key allowed)
839
914
  if len(value) != 1:
@@ -859,10 +934,12 @@ def _validate_eval_reference(var_name: str, value: dict) -> str:
859
934
 
860
935
 
861
936
  def _get_default_shell_and_args() -> tuple[str, list[str]]:
862
- """Get default shell and args for current platform.
937
+ """
938
+ Get default shell and args for current platform.
863
939
 
864
940
  Returns:
865
- Tuple of (shell, args) for platform default
941
+ Tuple of (shell, args) for platform default
942
+ @athena: 68a19449a035
866
943
  """
867
944
  is_windows = platform.system() == "Windows"
868
945
  if is_windows:
@@ -877,19 +954,21 @@ def _resolve_eval_variable(
877
954
  recipe_file_path: Path,
878
955
  recipe_data: dict
879
956
  ) -> str:
880
- """Execute command and capture output for variable value.
957
+ """
958
+ Execute command and capture output for variable value.
881
959
 
882
960
  Args:
883
- var_name: Name of the variable being defined
884
- command: Command to execute
885
- recipe_file_path: Path to recipe file (for working directory)
886
- recipe_data: Parsed YAML data (for accessing default_env)
961
+ var_name: Name of the variable being defined
962
+ command: Command to execute
963
+ recipe_file_path: Path to recipe file (for working directory)
964
+ recipe_data: Parsed YAML data (for accessing default_env)
887
965
 
888
966
  Returns:
889
- Command stdout as string (with trailing newline stripped)
967
+ Command stdout as string (with trailing newline stripped)
890
968
 
891
969
  Raises:
892
- ValueError: If command fails or cannot be executed
970
+ ValueError: If command fails or cannot be executed
971
+ @athena: 647d3a310c77
893
972
  """
894
973
  # Determine shell to use
895
974
  shell = None
@@ -968,21 +1047,23 @@ def _resolve_variable_value(
968
1047
  file_path: Path,
969
1048
  recipe_data: dict | None = None
970
1049
  ) -> str:
971
- """Resolve a single variable value with circular reference detection.
1050
+ """
1051
+ Resolve a single variable value with circular reference detection.
972
1052
 
973
1053
  Args:
974
- name: Variable name being resolved
975
- raw_value: Raw value from YAML (int, str, bool, float, dict with env/read/eval)
976
- resolved: Dictionary of already-resolved variables
977
- resolution_stack: Stack of variables currently being resolved (for circular detection)
978
- file_path: Path to recipe file (for resolving relative file paths in { read: ... })
979
- recipe_data: Parsed YAML data (for accessing default_env in { eval: ... })
1054
+ name: Variable name being resolved
1055
+ raw_value: Raw value from YAML (int, str, bool, float, dict with env/read/eval)
1056
+ resolved: Dictionary of already-resolved variables
1057
+ resolution_stack: Stack of variables currently being resolved (for circular detection)
1058
+ file_path: Path to recipe file (for resolving relative file paths in { read: ... })
1059
+ recipe_data: Parsed YAML data (for accessing default_env in { eval: ... })
980
1060
 
981
1061
  Returns:
982
- Resolved string value
1062
+ Resolved string value
983
1063
 
984
1064
  Raises:
985
- ValueError: If circular reference detected or validation fails
1065
+ ValueError: If circular reference detected or validation fails
1066
+ @athena: da94de106756
986
1067
  """
987
1068
  # Check for circular reference
988
1069
  if name in resolution_stack:
@@ -1114,20 +1195,22 @@ def _resolve_variable_value(
1114
1195
 
1115
1196
 
1116
1197
  def _parse_variables_section(data: dict, file_path: Path) -> dict[str, str]:
1117
- """Parse and resolve the variables section from YAML data.
1198
+ """
1199
+ Parse and resolve the variables section from YAML data.
1118
1200
 
1119
1201
  Variables are resolved in order, allowing variables to reference
1120
1202
  previously-defined variables using {{ var.name }} syntax.
1121
1203
 
1122
1204
  Args:
1123
- data: Parsed YAML data (root level)
1124
- file_path: Path to the recipe file (for resolving relative file paths)
1205
+ data: Parsed YAML data (root level)
1206
+ file_path: Path to the recipe file (for resolving relative file paths)
1125
1207
 
1126
1208
  Returns:
1127
- Dictionary mapping variable names to resolved string values
1209
+ Dictionary mapping variable names to resolved string values
1128
1210
 
1129
1211
  Raises:
1130
- ValueError: For validation errors, undefined refs, or circular refs
1212
+ ValueError: For validation errors, undefined refs, or circular refs
1213
+ @athena: aa45e860a958
1131
1214
  """
1132
1215
  if "variables" not in data:
1133
1216
  return {}
@@ -1152,26 +1235,28 @@ def _expand_variable_dependencies(
1152
1235
  variable_names: set[str],
1153
1236
  raw_variables: dict[str, Any]
1154
1237
  ) -> set[str]:
1155
- """Expand variable set to include all transitively referenced variables.
1238
+ """
1239
+ Expand variable set to include all transitively referenced variables.
1156
1240
 
1157
1241
  If variable A references variable B, and B references C, then requesting A
1158
1242
  should also evaluate B and C.
1159
1243
 
1160
1244
  Args:
1161
- variable_names: Initial set of variable names
1162
- raw_variables: Raw variable definitions from YAML
1245
+ variable_names: Initial set of variable names
1246
+ raw_variables: Raw variable definitions from YAML
1163
1247
 
1164
1248
  Returns:
1165
- Expanded set including all transitively referenced variables
1249
+ Expanded set including all transitively referenced variables
1166
1250
 
1167
1251
  Example:
1168
- >>> raw_vars = {
1169
- ... "a": "{{ var.b }}",
1170
- ... "b": "{{ var.c }}",
1171
- ... "c": "value"
1172
- ... }
1173
- >>> _expand_variable_dependencies({"a"}, raw_vars)
1174
- {"a", "b", "c"}
1252
+ >>> raw_vars = {
1253
+ ... "a": "{{ var.b }}",
1254
+ ... "b": "{{ var.c }}",
1255
+ ... "c": "value"
1256
+ ... }
1257
+ >>> _expand_variable_dependencies({"a"}, raw_vars)
1258
+ {"a", "b", "c"}
1259
+ @athena: e7ec4a4a6db3
1175
1260
  """
1176
1261
  expanded = set(variable_names)
1177
1262
  to_process = list(variable_names)
@@ -1235,7 +1320,8 @@ def _evaluate_variable_subset(
1235
1320
  file_path: Path,
1236
1321
  data: dict
1237
1322
  ) -> dict[str, str]:
1238
- """Evaluate only specified variables from raw specs (for lazy evaluation).
1323
+ """
1324
+ Evaluate only specified variables from raw specs (for lazy evaluation).
1239
1325
 
1240
1326
  This function is similar to _parse_variables_section but only evaluates
1241
1327
  a subset of variables. This enables lazy evaluation where only reachable
@@ -1245,21 +1331,22 @@ def _evaluate_variable_subset(
1245
1331
  variable B, both will be evaluated even if only A was explicitly requested.
1246
1332
 
1247
1333
  Args:
1248
- raw_variables: Raw variable definitions from YAML (not yet evaluated)
1249
- variable_names: Set of variable names to evaluate
1250
- file_path: Recipe file path (for relative file resolution)
1251
- data: Full YAML data (for context in _resolve_variable_value)
1334
+ raw_variables: Raw variable definitions from YAML (not yet evaluated)
1335
+ variable_names: Set of variable names to evaluate
1336
+ file_path: Recipe file path (for relative file resolution)
1337
+ data: Full YAML data (for context in _resolve_variable_value)
1252
1338
 
1253
1339
  Returns:
1254
- Dictionary of evaluated variable values (for specified variables and their dependencies)
1340
+ Dictionary of evaluated variable values (for specified variables and their dependencies)
1255
1341
 
1256
1342
  Raises:
1257
- ValueError: For validation errors, undefined refs, or circular refs
1343
+ ValueError: For validation errors, undefined refs, or circular refs
1258
1344
 
1259
1345
  Example:
1260
- >>> raw_vars = {"a": "{{ var.b }}", "b": "value", "c": "unused"}
1261
- >>> _evaluate_variable_subset(raw_vars, {"a"}, path, data)
1262
- {"a": "value", "b": "value"} # "a" and its dependency "b", but not "c"
1346
+ >>> raw_vars = {"a": "{{ var.b }}", "b": "value", "c": "unused"}
1347
+ >>> _evaluate_variable_subset(raw_vars, {"a"}, path, data)
1348
+ {"a": "value", "b": "value"} # "a" and its dependency "b", but not "c"
1349
+ @athena: 1e9d491d7404
1263
1350
  """
1264
1351
  if not isinstance(raw_variables, dict):
1265
1352
  raise ValueError("'variables' must be a dictionary")
@@ -1287,17 +1374,19 @@ def _parse_file_with_env(
1287
1374
  project_root: Path,
1288
1375
  import_stack: list[Path] | None = None,
1289
1376
  ) -> tuple[dict[str, Task], dict[str, Environment], str, dict[str, Any], dict[str, Any]]:
1290
- """Parse file and extract tasks, environments, and variables.
1377
+ """
1378
+ Parse file and extract tasks, environments, and variables.
1291
1379
 
1292
1380
  Args:
1293
- file_path: Path to YAML file
1294
- namespace: Optional namespace prefix for tasks
1295
- project_root: Root directory of the project
1296
- import_stack: Stack of files being imported (for circular detection)
1381
+ file_path: Path to YAML file
1382
+ namespace: Optional namespace prefix for tasks
1383
+ project_root: Root directory of the project
1384
+ import_stack: Stack of files being imported (for circular detection)
1297
1385
 
1298
1386
  Returns:
1299
- Tuple of (tasks, environments, default_env_name, raw_variables, yaml_data)
1300
- Note: Variables are NOT evaluated here - they're stored as raw specs for lazy evaluation
1387
+ Tuple of (tasks, environments, default_env_name, raw_variables, yaml_data)
1388
+ Note: Variables are NOT evaluated here - they're stored as raw specs for lazy evaluation
1389
+ @athena: b2dced506787
1301
1390
  """
1302
1391
  # Parse tasks normally
1303
1392
  tasks = _parse_file(file_path, namespace, project_root, import_stack)
@@ -1420,25 +1509,27 @@ def _parse_file_with_env(
1420
1509
 
1421
1510
 
1422
1511
  def collect_reachable_tasks(tasks: dict[str, Task], root_task: str) -> set[str]:
1423
- """Collect all tasks reachable from the root task via dependencies.
1512
+ """
1513
+ Collect all tasks reachable from the root task via dependencies.
1424
1514
 
1425
1515
  Uses BFS to traverse the dependency graph and collect all task names
1426
1516
  that could potentially be executed when running the root task.
1427
1517
 
1428
1518
  Args:
1429
- tasks: Dictionary mapping task names to Task objects
1430
- root_task: Name of the root task to start traversal from
1519
+ tasks: Dictionary mapping task names to Task objects
1520
+ root_task: Name of the root task to start traversal from
1431
1521
 
1432
1522
  Returns:
1433
- Set of task names reachable from root_task (includes root_task itself)
1523
+ Set of task names reachable from root_task (includes root_task itself)
1434
1524
 
1435
1525
  Raises:
1436
- ValueError: If root_task doesn't exist
1526
+ ValueError: If root_task doesn't exist
1437
1527
 
1438
1528
  Example:
1439
- >>> tasks = {"a": Task("a", deps=["b"]), "b": Task("b", deps=[]), "c": Task("c", deps=[])}
1440
- >>> collect_reachable_tasks(tasks, "a")
1441
- {"a", "b"}
1529
+ >>> tasks = {"a": Task("a", deps=["b"]), "b": Task("b", deps=[]), "c": Task("c", deps=[])}
1530
+ >>> collect_reachable_tasks(tasks, "a")
1531
+ {"a", "b"}
1532
+ @athena: fe29d8558be3
1442
1533
  """
1443
1534
  if root_task not in tasks:
1444
1535
  raise ValueError(f"Root task '{root_task}' not found in recipe")
@@ -1482,23 +1573,25 @@ def collect_reachable_variables(
1482
1573
  environments: dict[str, Environment],
1483
1574
  reachable_task_names: set[str]
1484
1575
  ) -> set[str]:
1485
- """Extract variable names used by reachable tasks.
1576
+ """
1577
+ Extract variable names used by reachable tasks.
1486
1578
 
1487
1579
  Searches for {{ var.* }} placeholders in task and environment definitions to determine
1488
1580
  which variables are actually needed for execution.
1489
1581
 
1490
1582
  Args:
1491
- tasks: Dictionary mapping task names to Task objects
1492
- environments: Dictionary mapping environment names to Environment objects
1493
- reachable_task_names: Set of task names that will be executed
1583
+ tasks: Dictionary mapping task names to Task objects
1584
+ environments: Dictionary mapping environment names to Environment objects
1585
+ reachable_task_names: Set of task names that will be executed
1494
1586
 
1495
1587
  Returns:
1496
- Set of variable names referenced by reachable tasks
1588
+ Set of variable names referenced by reachable tasks
1497
1589
 
1498
1590
  Example:
1499
- >>> task = Task("build", cmd="echo {{ var.version }}")
1500
- >>> collect_reachable_variables({"build": task}, {"build"})
1501
- {"version"}
1591
+ >>> task = Task("build", cmd="echo {{ var.version }}")
1592
+ >>> collect_reachable_variables({"build": task}, {"build"})
1593
+ {"version"}
1594
+ @athena: fc2a03927486
1502
1595
  """
1503
1596
  import re
1504
1597
 
@@ -1626,28 +1719,30 @@ def parse_recipe(
1626
1719
  project_root: Path | None = None,
1627
1720
  root_task: str | None = None
1628
1721
  ) -> Recipe:
1629
- """Parse a recipe file and handle imports recursively.
1722
+ """
1723
+ Parse a recipe file and handle imports recursively.
1630
1724
 
1631
1725
  This function now implements lazy variable evaluation: if root_task is provided,
1632
1726
  only variables reachable from that task will be evaluated. This provides significant
1633
1727
  performance and security benefits for recipes with many variables.
1634
1728
 
1635
1729
  Args:
1636
- recipe_path: Path to the main recipe file
1637
- project_root: Optional project root directory. If not provided, uses recipe file's parent directory.
1638
- When using --tasks option, this should be the current working directory.
1639
- root_task: Optional root task for lazy variable evaluation. If provided, only variables
1640
- used by tasks reachable from root_task will be evaluated (optimization).
1641
- If None, all variables will be evaluated (for --list command compatibility).
1730
+ recipe_path: Path to the main recipe file
1731
+ project_root: Optional project root directory. If not provided, uses recipe file's parent directory.
1732
+ When using --tasks option, this should be the current working directory.
1733
+ root_task: Optional root task for lazy variable evaluation. If provided, only variables
1734
+ used by tasks reachable from root_task will be evaluated (optimization).
1735
+ If None, all variables will be evaluated (for --list command compatibility).
1642
1736
 
1643
1737
  Returns:
1644
- Recipe object with all tasks (including recursively imported tasks) and evaluated variables
1738
+ Recipe object with all tasks (including recursively imported tasks) and evaluated variables
1645
1739
 
1646
1740
  Raises:
1647
- FileNotFoundError: If recipe file doesn't exist
1648
- CircularImportError: If circular imports are detected
1649
- yaml.YAMLError: If YAML is invalid
1650
- ValueError: If recipe structure is invalid
1741
+ FileNotFoundError: If recipe file doesn't exist
1742
+ CircularImportError: If circular imports are detected
1743
+ yaml.YAMLError: If YAML is invalid
1744
+ ValueError: If recipe structure is invalid
1745
+ @athena: 27326e37d5f3
1651
1746
  """
1652
1747
  if not recipe_path.exists():
1653
1748
  raise FileNotFoundError(f"Recipe file not found: {recipe_path}")
@@ -1690,21 +1785,23 @@ def _parse_file(
1690
1785
  project_root: Path,
1691
1786
  import_stack: list[Path] | None = None,
1692
1787
  ) -> dict[str, Task]:
1693
- """Parse a single YAML file and return tasks, recursively processing imports.
1788
+ """
1789
+ Parse a single YAML file and return tasks, recursively processing imports.
1694
1790
 
1695
1791
  Args:
1696
- file_path: Path to YAML file
1697
- namespace: Optional namespace prefix for tasks
1698
- project_root: Root directory of the project
1699
- import_stack: Stack of files being imported (for circular detection)
1792
+ file_path: Path to YAML file
1793
+ namespace: Optional namespace prefix for tasks
1794
+ project_root: Root directory of the project
1795
+ import_stack: Stack of files being imported (for circular detection)
1700
1796
 
1701
1797
  Returns:
1702
- Dictionary of task name to Task objects
1798
+ Dictionary of task name to Task objects
1703
1799
 
1704
1800
  Raises:
1705
- CircularImportError: If a circular import is detected
1706
- FileNotFoundError: If an imported file doesn't exist
1707
- ValueError: If task structure is invalid
1801
+ CircularImportError: If a circular import is detected
1802
+ FileNotFoundError: If an imported file doesn't exist
1803
+ ValueError: If task structure is invalid
1804
+ @athena: f2a1f2c70240
1708
1805
  """
1709
1806
  # Initialize import stack if not provided
1710
1807
  if import_stack is None:
@@ -1893,15 +1990,17 @@ def _parse_file(
1893
1990
 
1894
1991
 
1895
1992
  def _check_case_sensitive_arg_collisions(args: list[str], task_name: str) -> None:
1896
- """Check for exported arguments that differ only in case.
1993
+ """
1994
+ Check for exported arguments that differ only in case.
1897
1995
 
1898
1996
  On Unix systems, environment variables are case-sensitive, but having
1899
1997
  args that differ only in case (e.g., $Server and $server) can be confusing.
1900
1998
  This function emits a warning if such collisions are detected.
1901
1999
 
1902
2000
  Args:
1903
- args: List of argument specifications
1904
- task_name: Name of the task (for warning message)
2001
+ args: List of argument specifications
2002
+ task_name: Name of the task (for warning message)
2003
+ @athena: a3f0f3b184a8
1905
2004
  """
1906
2005
  import sys
1907
2006
 
@@ -1931,41 +2030,43 @@ def _check_case_sensitive_arg_collisions(args: list[str], task_name: str) -> Non
1931
2030
 
1932
2031
 
1933
2032
  def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
1934
- """Parse argument specification from YAML.
2033
+ """
2034
+ Parse argument specification from YAML.
1935
2035
 
1936
2036
  Supports both string format and dictionary format:
1937
2037
 
1938
2038
  String format (simple names only):
1939
- - Simple name: "argname"
1940
- - Exported (becomes env var): "$argname"
2039
+ - Simple name: "argname"
2040
+ - Exported (becomes env var): "$argname"
1941
2041
 
1942
2042
  Dictionary format:
1943
- - argname: { default: "value" }
1944
- - argname: { type: int, default: 42 }
1945
- - argname: { type: int, min: 1, max: 100 }
1946
- - argname: { type: str, choices: ["dev", "staging", "prod"] }
1947
- - $argname: { default: "value" } # Exported (type not allowed)
2043
+ - argname: { default: "value" }
2044
+ - argname: { type: int, default: 42 }
2045
+ - argname: { type: int, min: 1, max: 100 }
2046
+ - argname: { type: str, choices: ["dev", "staging", "prod"] }
2047
+ - $argname: { default: "value" } # Exported (type not allowed)
1948
2048
 
1949
2049
  Args:
1950
- arg_spec: Argument specification (string or dict with single key)
2050
+ arg_spec: Argument specification (string or dict with single key)
1951
2051
 
1952
2052
  Returns:
1953
- ArgSpec object containing parsed argument information
2053
+ ArgSpec object containing parsed argument information
1954
2054
 
1955
2055
  Examples:
1956
- >>> parse_arg_spec("environment")
1957
- ArgSpec(name='environment', arg_type='str', default=None, is_exported=False, min_val=None, max_val=None, choices=None)
1958
- >>> parse_arg_spec({"key2": {"default": "foo"}})
1959
- ArgSpec(name='key2', arg_type='str', default='foo', is_exported=False, min_val=None, max_val=None, choices=None)
1960
- >>> parse_arg_spec({"key3": {"type": "int", "default": 42}})
1961
- ArgSpec(name='key3', arg_type='int', default='42', is_exported=False, min_val=None, max_val=None, choices=None)
1962
- >>> parse_arg_spec({"replicas": {"type": "int", "min": 1, "max": 100}})
1963
- ArgSpec(name='replicas', arg_type='int', default=None, is_exported=False, min_val=1, max_val=100, choices=None)
1964
- >>> parse_arg_spec({"env": {"type": "str", "choices": ["dev", "prod"]}})
1965
- ArgSpec(name='env', arg_type='str', default=None, is_exported=False, min_val=None, max_val=None, choices=['dev', 'prod'])
2056
+ >>> parse_arg_spec("environment")
2057
+ ArgSpec(name='environment', arg_type='str', default=None, is_exported=False, min_val=None, max_val=None, choices=None)
2058
+ >>> parse_arg_spec({"key2": {"default": "foo"}})
2059
+ ArgSpec(name='key2', arg_type='str', default='foo', is_exported=False, min_val=None, max_val=None, choices=None)
2060
+ >>> parse_arg_spec({"key3": {"type": "int", "default": 42}})
2061
+ ArgSpec(name='key3', arg_type='int', default='42', is_exported=False, min_val=None, max_val=None, choices=None)
2062
+ >>> parse_arg_spec({"replicas": {"type": "int", "min": 1, "max": 100}})
2063
+ ArgSpec(name='replicas', arg_type='int', default=None, is_exported=False, min_val=1, max_val=100, choices=None)
2064
+ >>> parse_arg_spec({"env": {"type": "str", "choices": ["dev", "prod"]}})
2065
+ ArgSpec(name='env', arg_type='str', default=None, is_exported=False, min_val=None, max_val=None, choices=['dev', 'prod'])
1966
2066
 
1967
2067
  Raises:
1968
- ValueError: If argument specification is invalid
2068
+ ValueError: If argument specification is invalid
2069
+ @athena: 2a4c7e804622
1969
2070
  """
1970
2071
  # Handle dictionary format: { argname: { type: ..., default: ... } }
1971
2072
  if isinstance(arg_spec, dict):
@@ -2028,18 +2129,20 @@ def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
2028
2129
 
2029
2130
 
2030
2131
  def _parse_arg_dict(arg_name: str, config: dict, is_exported: bool) -> ArgSpec:
2031
- """Parse argument specification from dictionary format.
2132
+ """
2133
+ Parse argument specification from dictionary format.
2032
2134
 
2033
2135
  Args:
2034
- arg_name: Name of the argument
2035
- config: Dictionary with optional keys: type, default, min, max, choices
2036
- is_exported: Whether argument should be exported to environment
2136
+ arg_name: Name of the argument
2137
+ config: Dictionary with optional keys: type, default, min, max, choices
2138
+ is_exported: Whether argument should be exported to environment
2037
2139
 
2038
2140
  Returns:
2039
- ArgSpec object containing the parsed argument specification
2141
+ ArgSpec object containing the parsed argument specification
2040
2142
 
2041
2143
  Raises:
2042
- ValueError: If dictionary format is invalid
2144
+ ValueError: If dictionary format is invalid
2145
+ @athena: 5b6b93a3612a
2043
2146
  """
2044
2147
  # Validate dictionary keys
2045
2148
  valid_keys = {"type", "default", "min", "max", "choices"}
@@ -2275,7 +2378,8 @@ def _parse_arg_dict(arg_name: str, config: dict, is_exported: bool) -> ArgSpec:
2275
2378
 
2276
2379
 
2277
2380
  def parse_dependency_spec(dep_spec: str | dict[str, Any], recipe: Recipe) -> DependencyInvocation:
2278
- """Parse a dependency specification into a DependencyInvocation.
2381
+ """
2382
+ Parse a dependency specification into a DependencyInvocation.
2279
2383
 
2280
2384
  Supports three forms:
2281
2385
  1. Simple string: "task_name" -> DependencyInvocation(task_name, None)
@@ -2283,14 +2387,15 @@ def parse_dependency_spec(dep_spec: str | dict[str, Any], recipe: Recipe) -> Dep
2283
2387
  3. Named args: {"task_name": {arg1: val1}} -> DependencyInvocation(task_name, {arg1: val1})
2284
2388
 
2285
2389
  Args:
2286
- dep_spec: Dependency specification (string or dict)
2287
- recipe: Recipe containing task definitions (for arg normalization)
2390
+ dep_spec: Dependency specification (string or dict)
2391
+ recipe: Recipe containing task definitions (for arg normalization)
2288
2392
 
2289
2393
  Returns:
2290
- DependencyInvocation object with normalized args
2394
+ DependencyInvocation object with normalized args
2291
2395
 
2292
2396
  Raises:
2293
- ValueError: If dependency specification is invalid
2397
+ ValueError: If dependency specification is invalid
2398
+ @athena: d30ff06259c2
2294
2399
  """
2295
2400
  # Simple string case
2296
2401
  if isinstance(dep_spec, str):
@@ -2339,17 +2444,19 @@ def parse_dependency_spec(dep_spec: str | dict[str, Any], recipe: Recipe) -> Dep
2339
2444
 
2340
2445
 
2341
2446
  def _get_validated_task(task_name: str, recipe: Recipe) -> Task:
2342
- """Get and validate that a task exists in the recipe.
2447
+ """
2448
+ Get and validate that a task exists in the recipe.
2343
2449
 
2344
2450
  Args:
2345
- task_name: Name of the task to retrieve
2346
- recipe: Recipe containing task definitions
2451
+ task_name: Name of the task to retrieve
2452
+ recipe: Recipe containing task definitions
2347
2453
 
2348
2454
  Returns:
2349
- The validated Task object
2455
+ The validated Task object
2350
2456
 
2351
2457
  Raises:
2352
- ValueError: If task is not found
2458
+ ValueError: If task is not found
2459
+ @athena: 674f077e3977
2353
2460
  """
2354
2461
  task = recipe.get_task(task_name)
2355
2462
  if task is None:
@@ -2360,18 +2467,20 @@ def _get_validated_task(task_name: str, recipe: Recipe) -> Task:
2360
2467
  def _parse_positional_dependency_args(
2361
2468
  task_name: str, args_list: list[Any], recipe: Recipe
2362
2469
  ) -> DependencyInvocation:
2363
- """Parse positional dependency arguments.
2470
+ """
2471
+ Parse positional dependency arguments.
2364
2472
 
2365
2473
  Args:
2366
- task_name: Name of the dependency task
2367
- args_list: List of positional argument values
2368
- recipe: Recipe containing task definitions
2474
+ task_name: Name of the dependency task
2475
+ args_list: List of positional argument values
2476
+ recipe: Recipe containing task definitions
2369
2477
 
2370
2478
  Returns:
2371
- DependencyInvocation with normalized named args
2479
+ DependencyInvocation with normalized named args
2372
2480
 
2373
2481
  Raises:
2374
- ValueError: If validation fails
2482
+ ValueError: If validation fails
2483
+ @athena: 4d1c7957e2dd
2375
2484
  """
2376
2485
  # Get the task to validate against
2377
2486
  task = _get_validated_task(task_name, recipe)
@@ -2420,18 +2529,20 @@ def _parse_positional_dependency_args(
2420
2529
  def _parse_named_dependency_args(
2421
2530
  task_name: str, args_dict: dict[str, Any], recipe: Recipe
2422
2531
  ) -> DependencyInvocation:
2423
- """Parse named dependency arguments.
2532
+ """
2533
+ Parse named dependency arguments.
2424
2534
 
2425
2535
  Args:
2426
- task_name: Name of the dependency task
2427
- args_dict: Dictionary of argument names to values
2428
- recipe: Recipe containing task definitions
2536
+ task_name: Name of the dependency task
2537
+ args_dict: Dictionary of argument names to values
2538
+ recipe: Recipe containing task definitions
2429
2539
 
2430
2540
  Returns:
2431
- DependencyInvocation with normalized args (defaults filled)
2541
+ DependencyInvocation with normalized args (defaults filled)
2432
2542
 
2433
2543
  Raises:
2434
- ValueError: If validation fails
2544
+ ValueError: If validation fails
2545
+ @athena: c522211de525
2435
2546
  """
2436
2547
  # Get the task to validate against
2437
2548
  task = _get_validated_task(task_name, recipe)