tasktree 0.0.19__py3-none-any.whl → 0.0.20__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/__init__.py +2 -0
- tasktree/cli.py +12 -4
- tasktree/executor.py +14 -2
- tasktree/graph.py +61 -0
- tasktree/parser.py +68 -4
- tasktree/substitution.py +75 -2
- {tasktree-0.0.19.dist-info → tasktree-0.0.20.dist-info}/METADATA +236 -1
- tasktree-0.0.20.dist-info/RECORD +14 -0
- tasktree-0.0.19.dist-info/RECORD +0 -14
- {tasktree-0.0.19.dist-info → tasktree-0.0.20.dist-info}/WHEEL +0 -0
- {tasktree-0.0.19.dist-info → tasktree-0.0.20.dist-info}/entry_points.txt +0 -0
tasktree/__init__.py
CHANGED
|
@@ -15,6 +15,7 @@ from tasktree.graph import (
|
|
|
15
15
|
get_implicit_inputs,
|
|
16
16
|
resolve_dependency_output_references,
|
|
17
17
|
resolve_execution_order,
|
|
18
|
+
resolve_self_references,
|
|
18
19
|
)
|
|
19
20
|
from tasktree.hasher import hash_args, hash_task, make_cache_key
|
|
20
21
|
from tasktree.parser import Recipe, Task, find_recipe_file, parse_arg_spec, parse_recipe
|
|
@@ -31,6 +32,7 @@ __all__ = [
|
|
|
31
32
|
"get_implicit_inputs",
|
|
32
33
|
"resolve_dependency_output_references",
|
|
33
34
|
"resolve_execution_order",
|
|
35
|
+
"resolve_self_references",
|
|
34
36
|
"hash_args",
|
|
35
37
|
"hash_task",
|
|
36
38
|
"make_cache_key",
|
tasktree/cli.py
CHANGED
|
@@ -14,7 +14,7 @@ from rich.tree import Tree
|
|
|
14
14
|
|
|
15
15
|
from tasktree import __version__
|
|
16
16
|
from tasktree.executor import Executor
|
|
17
|
-
from tasktree.graph import build_dependency_tree, resolve_execution_order, resolve_dependency_output_references
|
|
17
|
+
from tasktree.graph import build_dependency_tree, resolve_execution_order, resolve_dependency_output_references, resolve_self_references
|
|
18
18
|
from tasktree.hasher import hash_task, hash_args
|
|
19
19
|
from tasktree.parser import Recipe, find_recipe_file, parse_arg_spec, parse_recipe
|
|
20
20
|
from tasktree.state import StateManager
|
|
@@ -464,9 +464,17 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
|
|
|
464
464
|
# This is important for correct state pruning after template substitution
|
|
465
465
|
execution_order = resolve_execution_order(recipe, task_name, args_dict)
|
|
466
466
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
467
|
+
try:
|
|
468
|
+
# Resolve dependency output references in topological order
|
|
469
|
+
# This substitutes {{ dep.*.outputs.* }} templates before execution
|
|
470
|
+
resolve_dependency_output_references(recipe, execution_order)
|
|
471
|
+
|
|
472
|
+
# Resolve self-references in topological order
|
|
473
|
+
# This substitutes {{ self.inputs.* }} and {{ self.outputs.* }} templates
|
|
474
|
+
resolve_self_references(recipe, execution_order)
|
|
475
|
+
except ValueError as e:
|
|
476
|
+
console.print(f"[red]Error in task template: {e}[/red]")
|
|
477
|
+
raise typer.Exit(1)
|
|
470
478
|
|
|
471
479
|
# Prune state based on tasks that will actually execute (with their specific arguments)
|
|
472
480
|
# This ensures template-substituted dependencies are handled correctly
|
tasktree/executor.py
CHANGED
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
from tasktree import docker as docker_module
|
|
17
|
-
from tasktree.graph import get_implicit_inputs, resolve_execution_order, resolve_dependency_output_references
|
|
17
|
+
from tasktree.graph import get_implicit_inputs, resolve_execution_order, resolve_dependency_output_references, resolve_self_references
|
|
18
18
|
from tasktree.hasher import hash_args, hash_task, make_cache_key
|
|
19
19
|
from tasktree.parser import Recipe, Task, Environment
|
|
20
20
|
from tasktree.state import StateManager, TaskState
|
|
@@ -437,6 +437,10 @@ class Executor:
|
|
|
437
437
|
# This substitutes {{ dep.*.outputs.* }} templates before execution
|
|
438
438
|
resolve_dependency_output_references(self.recipe, execution_order)
|
|
439
439
|
|
|
440
|
+
# Resolve self-references in topological order
|
|
441
|
+
# This substitutes {{ self.inputs.* }} and {{ self.outputs.* }} templates
|
|
442
|
+
resolve_self_references(self.recipe, execution_order)
|
|
443
|
+
|
|
440
444
|
# Single phase: Check and execute incrementally
|
|
441
445
|
statuses: dict[str, TaskStatus] = {}
|
|
442
446
|
for name, task_args in execution_order:
|
|
@@ -861,7 +865,15 @@ class Executor:
|
|
|
861
865
|
Returns:
|
|
862
866
|
List of input glob patterns
|
|
863
867
|
"""
|
|
864
|
-
|
|
868
|
+
# Extract paths from inputs (handle both anonymous strings and named dicts)
|
|
869
|
+
all_inputs = []
|
|
870
|
+
for inp in task.inputs:
|
|
871
|
+
if isinstance(inp, str):
|
|
872
|
+
all_inputs.append(inp)
|
|
873
|
+
elif isinstance(inp, dict):
|
|
874
|
+
# Named input - extract the path value(s)
|
|
875
|
+
all_inputs.extend(inp.values())
|
|
876
|
+
|
|
865
877
|
implicit_inputs = get_implicit_inputs(self.recipe, task)
|
|
866
878
|
all_inputs.extend(implicit_inputs)
|
|
867
879
|
return all_inputs
|
tasktree/graph.py
CHANGED
|
@@ -414,6 +414,67 @@ def resolve_dependency_output_references(
|
|
|
414
414
|
resolved_tasks[task_name] = task
|
|
415
415
|
|
|
416
416
|
|
|
417
|
+
def resolve_self_references(
|
|
418
|
+
recipe: Recipe,
|
|
419
|
+
ordered_tasks: list[tuple[str, dict[str, Any]]],
|
|
420
|
+
) -> None:
|
|
421
|
+
"""Resolve {{ self.inputs.name }} and {{ self.outputs.name }} references.
|
|
422
|
+
|
|
423
|
+
This function walks through tasks and resolves self-references to task's own
|
|
424
|
+
inputs/outputs. Must be called AFTER resolve_dependency_output_references()
|
|
425
|
+
so that dependency outputs are already resolved in output paths.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
recipe: Recipe containing task definitions
|
|
429
|
+
ordered_tasks: List of (task_name, args) tuples in topological order
|
|
430
|
+
|
|
431
|
+
Raises:
|
|
432
|
+
ValueError: If self-reference cannot be resolved (missing name, etc.)
|
|
433
|
+
|
|
434
|
+
Example:
|
|
435
|
+
If task.cmd contains "{{ self.inputs.src }}" and task has input {src: "*.txt"},
|
|
436
|
+
it will be resolved to "*.txt" (literal string, no glob expansion).
|
|
437
|
+
"""
|
|
438
|
+
from tasktree.substitution import substitute_self_references
|
|
439
|
+
|
|
440
|
+
for task_name, task_args in ordered_tasks:
|
|
441
|
+
task = recipe.tasks.get(task_name)
|
|
442
|
+
if task is None:
|
|
443
|
+
continue
|
|
444
|
+
|
|
445
|
+
# Resolve self-references in command
|
|
446
|
+
if task.cmd:
|
|
447
|
+
task.cmd = substitute_self_references(
|
|
448
|
+
task.cmd,
|
|
449
|
+
task_name,
|
|
450
|
+
task._input_map,
|
|
451
|
+
task._output_map,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Resolve self-references in working_dir
|
|
455
|
+
if task.working_dir:
|
|
456
|
+
task.working_dir = substitute_self_references(
|
|
457
|
+
task.working_dir,
|
|
458
|
+
task_name,
|
|
459
|
+
task._input_map,
|
|
460
|
+
task._output_map,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Resolve self-references in argument defaults
|
|
464
|
+
if task.args:
|
|
465
|
+
for arg_spec in task.args:
|
|
466
|
+
if isinstance(arg_spec, dict):
|
|
467
|
+
for arg_name, arg_details in arg_spec.items():
|
|
468
|
+
if isinstance(arg_details, dict) and "default" in arg_details:
|
|
469
|
+
if isinstance(arg_details["default"], str):
|
|
470
|
+
arg_details["default"] = substitute_self_references(
|
|
471
|
+
arg_details["default"],
|
|
472
|
+
task_name,
|
|
473
|
+
task._input_map,
|
|
474
|
+
task._output_map,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
417
478
|
def get_implicit_inputs(recipe: Recipe, task: Task) -> list[str]:
|
|
418
479
|
"""Get implicit inputs for a task based on its dependencies.
|
|
419
480
|
|
tasktree/parser.py
CHANGED
|
@@ -57,7 +57,7 @@ class Task:
|
|
|
57
57
|
cmd: str
|
|
58
58
|
desc: str = ""
|
|
59
59
|
deps: list[str | dict[str, Any]] = field(default_factory=list) # Can be strings or dicts with args
|
|
60
|
-
inputs: list[str] = field(default_factory=list)
|
|
60
|
+
inputs: list[str | dict[str, str]] = field(default_factory=list) # Can be strings or dicts with named inputs
|
|
61
61
|
outputs: list[str | dict[str, str]] = field(default_factory=list) # Can be strings or dicts with named outputs
|
|
62
62
|
working_dir: str = ""
|
|
63
63
|
args: list[str | dict[str, Any]] = field(default_factory=list) # Can be strings or dicts (each dict has single key: arg name)
|
|
@@ -69,6 +69,10 @@ class Task:
|
|
|
69
69
|
_output_map: dict[str, str] = field(init=False, default_factory=dict, repr=False) # name → path mapping
|
|
70
70
|
_anonymous_outputs: list[str] = field(init=False, default_factory=list, repr=False) # unnamed outputs
|
|
71
71
|
|
|
72
|
+
# Internal fields for efficient input lookup (built in __post_init__)
|
|
73
|
+
_input_map: dict[str, str] = field(init=False, default_factory=dict, repr=False) # name → path mapping
|
|
74
|
+
_anonymous_inputs: list[str] = field(init=False, default_factory=list, repr=False) # unnamed inputs
|
|
75
|
+
|
|
72
76
|
def __post_init__(self):
|
|
73
77
|
"""Ensure lists are always lists and build output maps."""
|
|
74
78
|
if isinstance(self.deps, str):
|
|
@@ -132,6 +136,45 @@ class Task:
|
|
|
132
136
|
f"Task '{self.name}': Output at index {idx} must be a string or dict, got {type(output).__name__}: {output}"
|
|
133
137
|
)
|
|
134
138
|
|
|
139
|
+
# Build input maps for efficient lookup
|
|
140
|
+
self._input_map = {}
|
|
141
|
+
self._anonymous_inputs = []
|
|
142
|
+
|
|
143
|
+
for idx, input_item in enumerate(self.inputs):
|
|
144
|
+
if isinstance(input_item, dict):
|
|
145
|
+
# Named input: validate and store
|
|
146
|
+
if len(input_item) != 1:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"Task '{self.name}': Named input at index {idx} must have exactly one key-value pair, got {len(input_item)}: {input_item}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
name, path = next(iter(input_item.items()))
|
|
152
|
+
|
|
153
|
+
if not isinstance(path, str):
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Task '{self.name}': Named input '{name}' must have a string path, got {type(path).__name__}: {path}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name):
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Task '{self.name}': Named input '{name}' must be a valid identifier "
|
|
161
|
+
f"(letters, numbers, underscores, cannot start with number)"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if name in self._input_map:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"Task '{self.name}': Duplicate input name '{name}' at index {idx}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self._input_map[name] = path
|
|
170
|
+
elif isinstance(input_item, str):
|
|
171
|
+
# Anonymous input: just store
|
|
172
|
+
self._anonymous_inputs.append(input_item)
|
|
173
|
+
else:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"Task '{self.name}': Input at index {idx} must be a string or dict, got {type(input_item).__name__}: {input_item}"
|
|
176
|
+
)
|
|
177
|
+
|
|
135
178
|
|
|
136
179
|
@dataclass
|
|
137
180
|
class DependencySpec:
|
|
@@ -309,7 +352,21 @@ class Recipe:
|
|
|
309
352
|
task.cmd = substitute_variables(task.cmd, self.evaluated_variables)
|
|
310
353
|
task.desc = substitute_variables(task.desc, self.evaluated_variables)
|
|
311
354
|
task.working_dir = substitute_variables(task.working_dir, self.evaluated_variables)
|
|
312
|
-
|
|
355
|
+
|
|
356
|
+
# Substitute variables in inputs (handle both string and dict inputs)
|
|
357
|
+
resolved_inputs = []
|
|
358
|
+
for inp in task.inputs:
|
|
359
|
+
if isinstance(inp, str):
|
|
360
|
+
resolved_inputs.append(substitute_variables(inp, self.evaluated_variables))
|
|
361
|
+
elif isinstance(inp, dict):
|
|
362
|
+
# Named input: substitute the path value
|
|
363
|
+
resolved_dict = {}
|
|
364
|
+
for name, path in inp.items():
|
|
365
|
+
resolved_dict[name] = substitute_variables(path, self.evaluated_variables)
|
|
366
|
+
resolved_inputs.append(resolved_dict)
|
|
367
|
+
else:
|
|
368
|
+
resolved_inputs.append(inp)
|
|
369
|
+
task.inputs = resolved_inputs
|
|
313
370
|
|
|
314
371
|
# Substitute variables in outputs (handle both string and dict outputs)
|
|
315
372
|
resolved_outputs = []
|
|
@@ -1473,8 +1530,15 @@ def collect_reachable_variables(
|
|
|
1473
1530
|
# Search in inputs
|
|
1474
1531
|
if task.inputs:
|
|
1475
1532
|
for input_pattern in task.inputs:
|
|
1476
|
-
|
|
1477
|
-
|
|
1533
|
+
if isinstance(input_pattern, str):
|
|
1534
|
+
for match in var_pattern.finditer(input_pattern):
|
|
1535
|
+
variables.add(match.group(1))
|
|
1536
|
+
elif isinstance(input_pattern, dict):
|
|
1537
|
+
# Named input - check the path value
|
|
1538
|
+
for input_path in input_pattern.values():
|
|
1539
|
+
if isinstance(input_path, str):
|
|
1540
|
+
for match in var_pattern.finditer(input_path):
|
|
1541
|
+
variables.add(match.group(1))
|
|
1478
1542
|
|
|
1479
1543
|
# Search in outputs
|
|
1480
1544
|
if task.outputs:
|
tasktree/substitution.py
CHANGED
|
@@ -12,13 +12,19 @@ from typing import Any
|
|
|
12
12
|
# Pattern matches: {{ prefix.name }} with optional whitespace
|
|
13
13
|
# Groups: (1) prefix (var|arg|env|tt), (2) name (identifier)
|
|
14
14
|
PLACEHOLDER_PATTERN = re.compile(
|
|
15
|
-
r'\{\{\s*(var|arg|env|tt)
|
|
15
|
+
r'\{\{\s*(var|arg|env|tt)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
# Pattern matches: {{ dep.task_name.outputs.output_name }} with optional whitespace
|
|
19
19
|
# Groups: (1) task_name (can include dots for namespacing), (2) output_name (identifier)
|
|
20
20
|
DEP_OUTPUT_PATTERN = re.compile(
|
|
21
|
-
r'\{\{\s*dep
|
|
21
|
+
r'\{\{\s*dep\.([a-zA-Z_][a-zA-Z0-9_.-]*)\.outputs\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Pattern matches: {{ self.(inputs|outputs).name }} with optional whitespace
|
|
25
|
+
# Groups: (1) field (inputs|outputs), (2) name (identifier)
|
|
26
|
+
SELF_REFERENCE_PATTERN = re.compile(
|
|
27
|
+
r'\{\{\s*self\.(inputs|outputs)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
|
|
22
28
|
)
|
|
23
29
|
|
|
24
30
|
|
|
@@ -372,3 +378,70 @@ def substitute_dependency_outputs(
|
|
|
372
378
|
return dep_task._output_map[output_name]
|
|
373
379
|
|
|
374
380
|
return DEP_OUTPUT_PATTERN.sub(replacer, text)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def substitute_self_references(
|
|
384
|
+
text: str,
|
|
385
|
+
task_name: str,
|
|
386
|
+
input_map: dict[str, str],
|
|
387
|
+
output_map: dict[str, str],
|
|
388
|
+
) -> str:
|
|
389
|
+
"""Substitute {{ self.inputs.name }} and {{ self.outputs.name }} placeholders.
|
|
390
|
+
|
|
391
|
+
This function resolves references to the task's own named inputs and outputs.
|
|
392
|
+
Only named entries are accessible; anonymous inputs/outputs cannot be referenced.
|
|
393
|
+
The substitution is literal string replacement - no glob expansion or path resolution.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
text: Text containing {{ self.* }} placeholders
|
|
397
|
+
task_name: Name of current task (for error messages)
|
|
398
|
+
input_map: Dictionary mapping input names to path strings
|
|
399
|
+
output_map: Dictionary mapping output names to path strings
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Text with all {{ self.* }} placeholders replaced with literal path strings
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
ValueError: If referenced name doesn't exist in input_map or output_map
|
|
406
|
+
|
|
407
|
+
Example:
|
|
408
|
+
>>> input_map = {"src": "*.txt"}
|
|
409
|
+
>>> output_map = {"dest": "out/result.txt"}
|
|
410
|
+
>>> substitute_self_references(
|
|
411
|
+
... "cp {{ self.inputs.src }} {{ self.outputs.dest }}",
|
|
412
|
+
... "copy",
|
|
413
|
+
... input_map,
|
|
414
|
+
... output_map
|
|
415
|
+
... )
|
|
416
|
+
'cp *.txt out/result.txt'
|
|
417
|
+
"""
|
|
418
|
+
def replacer(match: re.Match) -> str:
|
|
419
|
+
field = match.group(1) # "inputs" or "outputs"
|
|
420
|
+
name = match.group(2)
|
|
421
|
+
|
|
422
|
+
# Select appropriate map
|
|
423
|
+
if field == "inputs":
|
|
424
|
+
name_map = input_map
|
|
425
|
+
field_display = "input"
|
|
426
|
+
else: # field == "outputs"
|
|
427
|
+
name_map = output_map
|
|
428
|
+
field_display = "output"
|
|
429
|
+
|
|
430
|
+
# Check if name exists in map
|
|
431
|
+
if name not in name_map:
|
|
432
|
+
available = list(name_map.keys())
|
|
433
|
+
if available:
|
|
434
|
+
available_msg = ", ".join(available)
|
|
435
|
+
else:
|
|
436
|
+
available_msg = f"(none - all {field} are anonymous)"
|
|
437
|
+
|
|
438
|
+
raise ValueError(
|
|
439
|
+
f"Task '{task_name}' references {field_display} '{name}' "
|
|
440
|
+
f"but has no {field_display} named '{name}'.\n"
|
|
441
|
+
f"Available named {field}: {available_msg}\n"
|
|
442
|
+
f"Hint: Define named {field} like: {field}: [{{ {name}: 'path/to/file' }}]"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return name_map[name]
|
|
446
|
+
|
|
447
|
+
return SELF_REFERENCE_PATTERN.sub(replacer, text)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tasktree
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.20
|
|
4
4
|
Summary: A task automation tool with incremental execution
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: click>=8.1.0
|
|
@@ -833,6 +833,241 @@ Hint: Define named outputs like: outputs: [{ missing: 'path/to/file' }]
|
|
|
833
833
|
- **Deployment pipelines**: Reference exact artifacts to deploy
|
|
834
834
|
- **Configuration propagation**: Pass generated config files through build stages
|
|
835
835
|
|
|
836
|
+
### Self-References
|
|
837
|
+
|
|
838
|
+
Tasks can reference their own named inputs and outputs using `{{ self.inputs.name }}` and `{{ self.outputs.name }}` templates. This eliminates repetition when paths contain variables or when tasks have multiple inputs/outputs.
|
|
839
|
+
|
|
840
|
+
**Named Inputs and Outputs:**
|
|
841
|
+
|
|
842
|
+
Just like dependency output references, inputs and outputs can have names:
|
|
843
|
+
|
|
844
|
+
```yaml
|
|
845
|
+
tasks:
|
|
846
|
+
process:
|
|
847
|
+
inputs:
|
|
848
|
+
- src: "data/input.json" # Named input
|
|
849
|
+
- config: "config.yaml" # Named input
|
|
850
|
+
outputs:
|
|
851
|
+
- result: "output/result.json" # Named output
|
|
852
|
+
- log: "output/process.log" # Named output
|
|
853
|
+
cmd: |
|
|
854
|
+
process-tool \
|
|
855
|
+
--input {{ self.inputs.src }} \
|
|
856
|
+
--config {{ self.inputs.config }} \
|
|
857
|
+
--output {{ self.outputs.result }} \
|
|
858
|
+
--log {{ self.outputs.log }}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
**Syntax:**
|
|
862
|
+
|
|
863
|
+
- **Defining named inputs**: `inputs: [{ name: "path/to/file" }]`
|
|
864
|
+
- **Defining named outputs**: `outputs: [{ name: "path/to/file" }]`
|
|
865
|
+
- **Referencing inputs**: `{{ self.inputs.input_name }}`
|
|
866
|
+
- **Referencing outputs**: `{{ self.outputs.output_name }}`
|
|
867
|
+
- **Mixed format**: Can combine named and anonymous inputs/outputs in the same task
|
|
868
|
+
|
|
869
|
+
**Why Use Self-References?**
|
|
870
|
+
|
|
871
|
+
Self-references follow the DRY (Don't Repeat Yourself) principle:
|
|
872
|
+
|
|
873
|
+
```yaml
|
|
874
|
+
# Without self-references - repetitive
|
|
875
|
+
tasks:
|
|
876
|
+
build:
|
|
877
|
+
inputs: [src/app-{{ var.version }}.c]
|
|
878
|
+
outputs: [build/app-{{ var.version }}.o]
|
|
879
|
+
cmd: gcc src/app-{{ var.version }}.c -o build/app-{{ var.version }}.o
|
|
880
|
+
|
|
881
|
+
# With self-references - DRY
|
|
882
|
+
tasks:
|
|
883
|
+
build:
|
|
884
|
+
inputs:
|
|
885
|
+
- source: src/app-{{ var.version }}.c
|
|
886
|
+
outputs:
|
|
887
|
+
- object: build/app-{{ var.version }}.o
|
|
888
|
+
cmd: gcc {{ self.inputs.source }} -o {{ self.outputs.object }}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**Working with Variables:**
|
|
892
|
+
|
|
893
|
+
Self-references work seamlessly with variables:
|
|
894
|
+
|
|
895
|
+
```yaml
|
|
896
|
+
variables:
|
|
897
|
+
platform: linux
|
|
898
|
+
arch: x86_64
|
|
899
|
+
|
|
900
|
+
tasks:
|
|
901
|
+
compile:
|
|
902
|
+
inputs:
|
|
903
|
+
- src: src/{{ var.platform }}/main.c
|
|
904
|
+
- header: include/{{ var.arch }}/defs.h
|
|
905
|
+
outputs:
|
|
906
|
+
- binary: build/{{ var.platform }}-{{ var.arch }}/app
|
|
907
|
+
cmd: |
|
|
908
|
+
gcc {{ self.inputs.src }} \
|
|
909
|
+
-include {{ self.inputs.header }} \
|
|
910
|
+
-o {{ self.outputs.binary }}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
Variables are evaluated first, then self-references substitute the expanded paths.
|
|
914
|
+
|
|
915
|
+
**Working with Arguments:**
|
|
916
|
+
|
|
917
|
+
Self-references work with parameterized tasks:
|
|
918
|
+
|
|
919
|
+
```yaml
|
|
920
|
+
tasks:
|
|
921
|
+
deploy:
|
|
922
|
+
args: [environment]
|
|
923
|
+
inputs:
|
|
924
|
+
- config: configs/{{ arg.environment }}/app.yaml
|
|
925
|
+
outputs:
|
|
926
|
+
- deployed: deployed-{{ arg.environment }}.yaml
|
|
927
|
+
cmd: |
|
|
928
|
+
validate {{ self.inputs.config }}
|
|
929
|
+
deploy {{ self.inputs.config }} > {{ self.outputs.deployed }}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
```bash
|
|
933
|
+
tt deploy production # Uses configs/production/app.yaml
|
|
934
|
+
tt deploy staging # Uses configs/staging/app.yaml
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
**Working with Dependency Outputs:**
|
|
938
|
+
|
|
939
|
+
Self-references and dependency references can be used together:
|
|
940
|
+
|
|
941
|
+
```yaml
|
|
942
|
+
tasks:
|
|
943
|
+
build:
|
|
944
|
+
outputs:
|
|
945
|
+
- artifact: dist/app.js
|
|
946
|
+
cmd: webpack build
|
|
947
|
+
|
|
948
|
+
package:
|
|
949
|
+
deps: [build]
|
|
950
|
+
inputs:
|
|
951
|
+
- manifest: package.json
|
|
952
|
+
outputs:
|
|
953
|
+
- tarball: release.tar.gz
|
|
954
|
+
cmd: tar czf {{ self.outputs.tarball }} \
|
|
955
|
+
{{ self.inputs.manifest }} \
|
|
956
|
+
{{ dep.build.outputs.artifact }}
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
**Mixed Named and Anonymous:**
|
|
960
|
+
|
|
961
|
+
Tasks can mix named and anonymous inputs/outputs:
|
|
962
|
+
|
|
963
|
+
```yaml
|
|
964
|
+
tasks:
|
|
965
|
+
build:
|
|
966
|
+
inputs:
|
|
967
|
+
- config: build.yaml # Named - can reference
|
|
968
|
+
- src/**/*.c # Anonymous - tracked but not referenceable
|
|
969
|
+
outputs:
|
|
970
|
+
- binary: bin/app # Named - can reference
|
|
971
|
+
- bin/app.debug # Anonymous - tracked but not referenceable
|
|
972
|
+
cmd: build-tool --config {{ self.inputs.config }} --output {{ self.outputs.binary }}
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
**Error Messages:**
|
|
976
|
+
|
|
977
|
+
If you reference a non-existent input or output:
|
|
978
|
+
|
|
979
|
+
```yaml
|
|
980
|
+
tasks:
|
|
981
|
+
build:
|
|
982
|
+
inputs:
|
|
983
|
+
- src: input.txt
|
|
984
|
+
cmd: cat {{ self.inputs.missing }} # Error!
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
You'll get a clear error before execution:
|
|
988
|
+
|
|
989
|
+
```
|
|
990
|
+
Task 'build' references input 'missing' but has no input named 'missing'.
|
|
991
|
+
Available named inputs: src
|
|
992
|
+
Hint: Define named inputs like: inputs: [{ missing: 'path/to/file' }]
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
Similarly, if you try to reference an anonymous input:
|
|
996
|
+
|
|
997
|
+
```yaml
|
|
998
|
+
tasks:
|
|
999
|
+
build:
|
|
1000
|
+
inputs: [file.txt] # Anonymous input
|
|
1001
|
+
cmd: cat {{ self.inputs.src }} # Error!
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
You'll get:
|
|
1005
|
+
|
|
1006
|
+
```
|
|
1007
|
+
Task 'build' references input 'src' but has no input named 'src'.
|
|
1008
|
+
Available named inputs: (none - all inputs are anonymous)
|
|
1009
|
+
Hint: Define named inputs like: inputs: [{ src: 'file.txt' }]
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
**Key Behaviors:**
|
|
1013
|
+
|
|
1014
|
+
- **Template resolution**: Self-references are resolved during dependency graph planning
|
|
1015
|
+
- **Substitution order**: Variables → Dependency outputs → Self-references → Arguments/Environment
|
|
1016
|
+
- **Fail-fast validation**: Errors are caught before execution begins
|
|
1017
|
+
- **Clear error messages**: Lists available names if reference doesn't exist
|
|
1018
|
+
- **Backward compatible**: Existing anonymous inputs/outputs work unchanged
|
|
1019
|
+
- **State tracking**: Works correctly with incremental execution and freshness checks
|
|
1020
|
+
|
|
1021
|
+
**Limitations:**
|
|
1022
|
+
|
|
1023
|
+
- **Anonymous not referenceable**: Only named inputs/outputs can be referenced
|
|
1024
|
+
- **Case sensitive**: `{{ self.inputs.Src }}` and `{{ self.inputs.src }}` are different
|
|
1025
|
+
- **Argument defaults**: Self-references in argument defaults are not supported (arguments are evaluated before self-references)
|
|
1026
|
+
|
|
1027
|
+
**Use Cases:**
|
|
1028
|
+
|
|
1029
|
+
- **Eliminate repetition**: Define complex paths once, use them multiple times
|
|
1030
|
+
- **Variable composition**: Combine variables with self-references for clean commands
|
|
1031
|
+
- **Multiple inputs/outputs**: Reference specific files when tasks have many
|
|
1032
|
+
- **Complex build pipelines**: Keep commands readable with named artifacts
|
|
1033
|
+
- **Glob patterns**: Use self-references with glob patterns for dynamic inputs
|
|
1034
|
+
|
|
1035
|
+
**Example: Multi-Stage Build:**
|
|
1036
|
+
|
|
1037
|
+
```yaml
|
|
1038
|
+
variables:
|
|
1039
|
+
version: "2.1.0"
|
|
1040
|
+
platform: "linux"
|
|
1041
|
+
|
|
1042
|
+
tasks:
|
|
1043
|
+
prepare:
|
|
1044
|
+
outputs:
|
|
1045
|
+
- builddir: build/{{ var.platform }}-{{ var.version }}
|
|
1046
|
+
cmd: mkdir -p {{ self.outputs.builddir }}
|
|
1047
|
+
|
|
1048
|
+
compile:
|
|
1049
|
+
deps: [prepare]
|
|
1050
|
+
inputs:
|
|
1051
|
+
- source: src/main.c
|
|
1052
|
+
- headers: include/*.h
|
|
1053
|
+
outputs:
|
|
1054
|
+
- object: build/{{ var.platform }}-{{ var.version }}/main.o
|
|
1055
|
+
cmd: |
|
|
1056
|
+
gcc -c {{ self.inputs.source }} \
|
|
1057
|
+
-I include \
|
|
1058
|
+
-o {{ self.outputs.object }}
|
|
1059
|
+
|
|
1060
|
+
link:
|
|
1061
|
+
deps: [compile]
|
|
1062
|
+
outputs:
|
|
1063
|
+
- executable: build/{{ var.platform }}-{{ var.version }}/app
|
|
1064
|
+
- symbols: build/{{ var.platform }}-{{ var.version }}/app.sym
|
|
1065
|
+
cmd: |
|
|
1066
|
+
gcc build/{{ var.platform }}-{{ var.version }}/main.o \
|
|
1067
|
+
-o {{ self.outputs.executable }}
|
|
1068
|
+
objcopy --only-keep-debug {{ self.outputs.executable }} {{ self.outputs.symbols }}
|
|
1069
|
+
```
|
|
1070
|
+
|
|
836
1071
|
|
|
837
1072
|
### Private Tasks
|
|
838
1073
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
tasktree/__init__.py,sha256=m7fLsPUft99oB_XXr4dOu2yUWu74zVutkw1-3zGrG5Y,1227
|
|
2
|
+
tasktree/cli.py,sha256=S5ypqQlvCxdAvlBfO8TJZhvMoc086wqgvmOm8220678,21220
|
|
3
|
+
tasktree/docker.py,sha256=qvja8G63uAcC73YMVY739egda1_CcBtoqzm0qIJU_Q8,14443
|
|
4
|
+
tasktree/executor.py,sha256=g4mHtoO3wVIxyqNALIdJOEwlEqRkSY0eY-6sl2jF-IA,46463
|
|
5
|
+
tasktree/graph.py,sha256=T78JH0whP7VquEvtOVN-8ePyHNcseTQoEouijDrgmkw,22663
|
|
6
|
+
tasktree/hasher.py,sha256=o7Akd_AgGkAsnv9biK0AcbhlcqUQ9ne5y_6r4zoFaw0,5493
|
|
7
|
+
tasktree/parser.py,sha256=R0swEkKBPGeijLHxD1CbQjtoKVn2gRJadsZuyKj1sdM,97922
|
|
8
|
+
tasktree/state.py,sha256=Cktl4D8iDZVd55aO2LqVyPrc-BnljkesxxkcMcdcfOY,3541
|
|
9
|
+
tasktree/substitution.py,sha256=FhtFI0ciK9bQxPdORvpf1coa59XxizKiBiUwHJp0PtI,16811
|
|
10
|
+
tasktree/types.py,sha256=R_YAyO5bMLB6XZnkMRT7VAtlkA_Xx6xu0aIpzQjrBXs,4357
|
|
11
|
+
tasktree-0.0.20.dist-info/METADATA,sha256=C8cdZtyz-dgiVoN1H2hy7wQNAA-K784WYFfqlZEOXnA,50237
|
|
12
|
+
tasktree-0.0.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
13
|
+
tasktree-0.0.20.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
|
|
14
|
+
tasktree-0.0.20.dist-info/RECORD,,
|
tasktree-0.0.19.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
tasktree/__init__.py,sha256=N-dZcggJDe4WaloC1MdEh0oMTHlUpBN9AEcAm6leDf4,1167
|
|
2
|
-
tasktree/cli.py,sha256=_GzqQIk2z2Hz17bkTOWhtNW4X4_Ijxn-uEL-WZGt8LM,20858
|
|
3
|
-
tasktree/docker.py,sha256=qvja8G63uAcC73YMVY739egda1_CcBtoqzm0qIJU_Q8,14443
|
|
4
|
-
tasktree/executor.py,sha256=7pzcH2wLWMZPk3hwhzWgz18RVkIPFCKdu23MboWUQs4,45914
|
|
5
|
-
tasktree/graph.py,sha256=9O5LByzMYa8ccedznqKBTb0Xe9N_aajSR1cAcb8zGQE,20366
|
|
6
|
-
tasktree/hasher.py,sha256=o7Akd_AgGkAsnv9biK0AcbhlcqUQ9ne5y_6r4zoFaw0,5493
|
|
7
|
-
tasktree/parser.py,sha256=PVgtGORCpnkb8wcXHFfsyVqDhJ3PwzwCqO3VWiuLQl4,94777
|
|
8
|
-
tasktree/state.py,sha256=Cktl4D8iDZVd55aO2LqVyPrc-BnljkesxxkcMcdcfOY,3541
|
|
9
|
-
tasktree/substitution.py,sha256=3-gdvHbBwPkQPflx3GVSpEEa0vTL_ivdcMIba77gtJc,14225
|
|
10
|
-
tasktree/types.py,sha256=R_YAyO5bMLB6XZnkMRT7VAtlkA_Xx6xu0aIpzQjrBXs,4357
|
|
11
|
-
tasktree-0.0.19.dist-info/METADATA,sha256=KTLp0p1Mkzzvah-Hy15KrXnTcKju5dlQyvDvEdnDOmg,43609
|
|
12
|
-
tasktree-0.0.19.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
13
|
-
tasktree-0.0.19.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
|
|
14
|
-
tasktree-0.0.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|