tasktree 0.0.6__tar.gz → 0.0.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {tasktree-0.0.6 → tasktree-0.0.7}/PKG-INFO +2 -47
  2. {tasktree-0.0.6 → tasktree-0.0.7}/README.md +0 -46
  3. {tasktree-0.0.6 → tasktree-0.0.7}/pyproject.toml +2 -1
  4. tasktree-0.0.7/requirements/bug-report-dependency-triggering.md +222 -0
  5. {tasktree-0.0.6 → tasktree-0.0.7}/schema/tasktree-schema.json +34 -2
  6. tasktree-0.0.7/src/tasktree/docker.py +413 -0
  7. {tasktree-0.0.6 → tasktree-0.0.7}/src/tasktree/executor.py +268 -31
  8. {tasktree-0.0.6 → tasktree-0.0.7}/src/tasktree/graph.py +30 -1
  9. tasktree-0.0.7/src/tasktree/hasher.py +54 -0
  10. {tasktree-0.0.6 → tasktree-0.0.7}/src/tasktree/parser.py +74 -8
  11. {tasktree-0.0.6 → tasktree-0.0.7}/src/tasktree/state.py +1 -1
  12. {tasktree-0.0.6 → tasktree-0.0.7}/tasktree.yaml +1 -1
  13. {tasktree-0.0.6 → tasktree-0.0.7}/tests/integration/test_dependency_execution.py +160 -0
  14. tasktree-0.0.7/tests/unit/test_docker.py +277 -0
  15. tasktree-0.0.7/tests/unit/test_environment_tracking.py +356 -0
  16. {tasktree-0.0.6 → tasktree-0.0.7}/tests/unit/test_parser.py +2 -1
  17. {tasktree-0.0.6 → tasktree-0.0.7}/uv.lock +11 -0
  18. tasktree-0.0.6/src/tasktree/hasher.py +0 -27
  19. {tasktree-0.0.6 → tasktree-0.0.7}/.github/workflows/release.yml +0 -0
  20. {tasktree-0.0.6 → tasktree-0.0.7}/.github/workflows/test.yml +0 -0
  21. {tasktree-0.0.6 → tasktree-0.0.7}/.gitignore +0 -0
  22. {tasktree-0.0.6 → tasktree-0.0.7}/.python-version +0 -0
  23. {tasktree-0.0.6 → tasktree-0.0.7}/CLAUDE.md +0 -0
  24. {tasktree-0.0.6 → tasktree-0.0.7}/example/source.txt +0 -0
  25. {tasktree-0.0.6 → tasktree-0.0.7}/example/tasktree.yaml +0 -0
  26. {tasktree-0.0.6 → tasktree-0.0.7}/requirements/future/docker-task-environments.md +0 -0
  27. {tasktree-0.0.6 → tasktree-0.0.7}/requirements/implemented/shell-environment-requirements.md +0 -0
  28. {tasktree-0.0.6 → tasktree-0.0.7}/schema/README.md +0 -0
  29. {tasktree-0.0.6 → tasktree-0.0.7}/schema/vscode-settings-snippet.json +0 -0
  30. {tasktree-0.0.6 → tasktree-0.0.7}/src/__init__.py +0 -0
  31. {tasktree-0.0.6 → tasktree-0.0.7}/src/tasktree/__init__.py +0 -0
  32. {tasktree-0.0.6 → tasktree-0.0.7}/src/tasktree/cli.py +0 -0
  33. {tasktree-0.0.6 → tasktree-0.0.7}/src/tasktree/tasks.py +0 -0
  34. {tasktree-0.0.6 → tasktree-0.0.7}/src/tasktree/types.py +0 -0
  35. {tasktree-0.0.6 → tasktree-0.0.7}/tests/integration/test_clean_state.py +0 -0
  36. {tasktree-0.0.6 → tasktree-0.0.7}/tests/integration/test_cli_options.py +0 -0
  37. {tasktree-0.0.6 → tasktree-0.0.7}/tests/integration/test_end_to_end.py +0 -0
  38. {tasktree-0.0.6 → tasktree-0.0.7}/tests/integration/test_input_detection.py +0 -0
  39. {tasktree-0.0.6 → tasktree-0.0.7}/tests/integration/test_missing_outputs.py +0 -0
  40. {tasktree-0.0.6 → tasktree-0.0.7}/tests/integration/test_nested_imports.py +0 -0
  41. {tasktree-0.0.6 → tasktree-0.0.7}/tests/integration/test_state_persistence.py +0 -0
  42. {tasktree-0.0.6 → tasktree-0.0.7}/tests/integration/test_working_directory.py +0 -0
  43. {tasktree-0.0.6 → tasktree-0.0.7}/tests/unit/test_cli.py +0 -0
  44. {tasktree-0.0.6 → tasktree-0.0.7}/tests/unit/test_executor.py +0 -0
  45. {tasktree-0.0.6 → tasktree-0.0.7}/tests/unit/test_graph.py +0 -0
  46. {tasktree-0.0.6 → tasktree-0.0.7}/tests/unit/test_hasher.py +0 -0
  47. {tasktree-0.0.6 → tasktree-0.0.7}/tests/unit/test_state.py +0 -0
  48. {tasktree-0.0.6 → tasktree-0.0.7}/tests/unit/test_tasks.py +0 -0
  49. {tasktree-0.0.6 → tasktree-0.0.7}/tests/unit/test_types.py +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
7
7
  Requires-Dist: colorama>=0.4.6
8
+ Requires-Dist: pathspec>=0.11.0
8
9
  Requires-Dist: pyyaml>=6.0
9
10
  Requires-Dist: rich>=13.0.0
10
11
  Requires-Dist: typer>=0.9.0
@@ -108,52 +109,6 @@ Boom! Done. `build` will always run, because there's no sensible way to know wha
108
109
 
109
110
  This is a toy example, but you can image how it plays out on a more complex project.
110
111
 
111
- ## Migrating from v1.x to v2.0
112
-
113
- Version 2.0 requires all task definitions to be under a top-level `tasks:` key.
114
-
115
- ### Quick Migration
116
-
117
- Wrap your existing tasks in a `tasks:` block:
118
-
119
- ```yaml
120
- # Before (v1.x)
121
- build:
122
- cmd: cargo build
123
-
124
- # After (v2.0)
125
- tasks:
126
- build:
127
- cmd: cargo build
128
- ```
129
-
130
- ### Why This Change?
131
-
132
- 1. **Clearer structure**: Explicit separation of tasks from configuration
133
- 2. **No naming conflicts**: You can now create tasks named "imports" or "environments"
134
- 3. **Better error messages**: More helpful validation errors
135
- 4. **Consistency**: All recipe files use the same format
136
-
137
- ### Error Messages
138
-
139
- If you forget to update, you'll see a clear error:
140
-
141
- ```
142
- Invalid recipe format in tasktree.yaml
143
-
144
- Task definitions must be under a top-level "tasks:" key.
145
-
146
- Found these keys at root level: build, test
147
-
148
- Did you mean:
149
-
150
- tasks:
151
- build:
152
- cmd: ...
153
- test:
154
- cmd: ...
155
- ```
156
-
157
112
  ## Installation
158
113
 
159
114
  ### From PyPI (Recommended)
@@ -94,52 +94,6 @@ Boom! Done. `build` will always run, because there's no sensible way to know wha
94
94
 
95
95
  This is a toy example, but you can image how it plays out on a more complex project.
96
96
 
97
- ## Migrating from v1.x to v2.0
98
-
99
- Version 2.0 requires all task definitions to be under a top-level `tasks:` key.
100
-
101
- ### Quick Migration
102
-
103
- Wrap your existing tasks in a `tasks:` block:
104
-
105
- ```yaml
106
- # Before (v1.x)
107
- build:
108
- cmd: cargo build
109
-
110
- # After (v2.0)
111
- tasks:
112
- build:
113
- cmd: cargo build
114
- ```
115
-
116
- ### Why This Change?
117
-
118
- 1. **Clearer structure**: Explicit separation of tasks from configuration
119
- 2. **No naming conflicts**: You can now create tasks named "imports" or "environments"
120
- 3. **Better error messages**: More helpful validation errors
121
- 4. **Consistency**: All recipe files use the same format
122
-
123
- ### Error Messages
124
-
125
- If you forget to update, you'll see a clear error:
126
-
127
- ```
128
- Invalid recipe format in tasktree.yaml
129
-
130
- Task definitions must be under a top-level "tasks:" key.
131
-
132
- Found these keys at root level: build, test
133
-
134
- Did you mean:
135
-
136
- tasks:
137
- build:
138
- cmd: ...
139
- test:
140
- cmd: ...
141
- ```
142
-
143
97
  ## Installation
144
98
 
145
99
  ### From PyPI (Recommended)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tasktree"
3
- version = "0.0.6"
3
+ version = "0.0.7"
4
4
  description = "A task automation tool with incremental execution"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "click>=8.1.0",
11
11
  "rich>=13.0.0",
12
12
  "colorama>=0.4.6",
13
+ "pathspec>=0.11.0",
13
14
  ]
14
15
 
15
16
  [project.optional-dependencies]
@@ -0,0 +1,222 @@
1
+ # Bug Report: False Dependency Triggering in Incremental Execution
2
+
3
+ ## Summary
4
+
5
+ Tasks are incorrectly triggered to run when their dependencies execute, even when those dependencies produce no actual changes to their outputs. This violates the principle of incremental execution and causes unnecessary rebuilds.
6
+
7
+ ## Current Behaviour
8
+
9
+ The executor performs static analysis of the dependency graph before execution:
10
+
11
+ 1. Topologically sorts all tasks
12
+ 2. For each task in order, checks if it should run by examining:
13
+ - Whether any dependency has `will_run=True` (static check)
14
+ - Whether inputs have changed (dynamic check via mtime)
15
+ - Other conditions (never run, no outputs, etc.)
16
+ 3. Executes all tasks marked with `will_run=True`
17
+
18
+ The problem occurs at step 2: if a dependency is marked `will_run=True`, the current task is immediately marked to run, regardless of whether that dependency actually modified any files.
19
+
20
+ ## Expected Behaviour
21
+
22
+ Tasks should run based solely on the runtime state of their inputs:
23
+
24
+ 1. Topologically sort all tasks
25
+ 2. For each task in execution order:
26
+ - Check if any of its inputs (explicit or implicit) have changed
27
+ - If inputs changed (or other valid conditions met), execute the task
28
+ - After execution, downstream tasks check their inputs against actual file mtimes
29
+ 3. A dependency that runs but produces no changes should NOT trigger downstream tasks
30
+
31
+ This matches Make's behaviour: it checks whether dependency output files are newer than the target, not whether the dependency command ran.
32
+
33
+ ## Reproduction Test Case
34
+
35
+ The following test demonstrates the bug:
36
+
37
+ ```python
38
+ def test_dependency_runs_but_produces_no_changes():
39
+ """
40
+ Test that a task whose dependency runs but produces no output changes
41
+ does NOT trigger re-execution.
42
+
43
+ Scenario:
44
+ - Task 'build' has no inputs, declares outputs (always runs, like cargo/make)
45
+ - Task 'build' runs but creates no new files (simulated by not touching)
46
+ - Task 'package' depends on 'build' (implicitly gets build outputs as inputs)
47
+ - Expected: 'package' should NOT run because build's outputs didn't change
48
+ - Actual (bug): 'package' runs because 'build' has will_run=True
49
+ """
50
+ import tempfile
51
+ import os
52
+ from pathlib import Path
53
+ from unittest.mock import patch
54
+ import yaml
55
+
56
+ with tempfile.TemporaryDirectory() as tmpdir:
57
+ project_root = Path(tmpdir)
58
+
59
+ # Create recipe file
60
+ recipe = {
61
+ 'build': {
62
+ 'desc': 'Simulate build tool (cargo/make) with internal dep resolution\nNo inputs - tool does its own dependency checking\nHas outputs so package can implicitly depend on them',
63
+ 'outputs': ['build-artifact.txt'],
64
+ 'cmd': 'touch build-artifact.txt',
65
+ },
66
+ 'package': {
67
+ 'desc': 'No explicit inputs - should implicitly inherit build-artifact.txt from build',
68
+ 'deps': ['build'],
69
+ 'outputs': ['package.tar.gz'],
70
+ 'cmd': 'touch package.tar.gz',
71
+ }
72
+ }
73
+
74
+ recipe_path = project_root / 'tasktree.yaml'
75
+ recipe_path.write_text(yaml.dump(recipe))
76
+
77
+ # First run: establish baseline
78
+ # This creates build-artifact.txt and package.tar.gz
79
+ from tasktree.parser import parse_recipe
80
+ from tasktree.state import StateManager
81
+ from tasktree.executor import Executor
82
+
83
+ parsed_recipe = parse_recipe(recipe_path)
84
+ state_manager = StateManager(project_root / '.tasktree-state')
85
+ executor = Executor(parsed_recipe, state_manager)
86
+
87
+ statuses = executor.execute_task('package')
88
+
89
+ assert statuses['build'].will_run # First run, no state
90
+ assert statuses['package'].will_run # First run, no state
91
+
92
+ # Verify files exist
93
+ assert (project_root / 'build-artifact.txt').exists()
94
+ assert (project_root / 'package.tar.gz').exists()
95
+
96
+ # Get actual mtime of build artifact
97
+ build_artifact_path = project_root / 'build-artifact.txt'
98
+ original_mtime = build_artifact_path.stat().st_mtime
99
+
100
+ # Second run: build task runs (no inputs) but produces no changes
101
+ # Simulate this by having build command do nothing
102
+ recipe['build']['cmd'] = 'echo "checking dependencies, nothing to do"'
103
+ recipe_path.write_text(yaml.dump(recipe))
104
+
105
+ parsed_recipe = parse_recipe(recipe_path)
106
+ executor = Executor(parsed_recipe, state_manager)
107
+
108
+ # Patch os.stat to return the original mtime for build-artifact.txt
109
+ # This simulates build running but not modifying its outputs
110
+ original_stat = os.stat
111
+ def patched_stat(path, *args, **kwargs):
112
+ result = original_stat(path, *args, **kwargs)
113
+ if Path(path) == build_artifact_path:
114
+ # Return stat result with original mtime
115
+ import os
116
+ stat_result = os.stat_result((
117
+ result.st_mode,
118
+ result.st_ino,
119
+ result.st_dev,
120
+ result.st_nlink,
121
+ result.st_uid,
122
+ result.st_gid,
123
+ result.st_size,
124
+ result.st_atime,
125
+ original_mtime, # Keep original mtime
126
+ result.st_ctime,
127
+ ))
128
+ return stat_result
129
+ return result
130
+
131
+ with patch('os.stat', side_effect=patched_stat):
132
+ with patch('pathlib.Path.stat', side_effect=lambda self: patched_stat(self)):
133
+ statuses = executor.execute_task('package')
134
+
135
+ # Build task should run (has no inputs)
136
+ assert statuses['build'].will_run
137
+ assert statuses['build'].reason == 'no_outputs'
138
+
139
+ # BUG: Package task runs because build ran, even though build's output unchanged
140
+ # EXPECTED: package task should NOT run (implicit inputs haven't changed)
141
+ assert statuses['package'].will_run == False, \
142
+ f"Package should not run when dependency produces no changes, " \
143
+ f"but will_run={statuses['package'].will_run}, reason={statuses['package'].reason}"
144
+ ```
145
+
146
+ ## Root Cause Analysis
147
+
148
+ The issue is in the execution planning phase. The code attempts to determine which tasks will run before any tasks execute. This creates a dependency on static analysis of the dependency graph rather than dynamic checking of actual file states.
149
+
150
+ The specific problematic logic:
151
+ - Checking `if any(status.will_run for status in dep_statuses.values())`
152
+ - This happens before the dependency actually runs
153
+ - Therefore, it cannot know what the dependency will actually do to files
154
+
155
+ ## Algorithmic Changes Required
156
+
157
+ The execution algorithm needs to shift from "plan then execute" to "check and execute incrementally":
158
+
159
+ **Current approach:**
160
+ ```
161
+ for each task in topo order:
162
+ if dependency.will_run:
163
+ mark this task to run
164
+ elif inputs changed:
165
+ mark this task to run
166
+
167
+ for each task marked to run:
168
+ execute task
169
+ ```
170
+
171
+ **Required approach:**
172
+ ```
173
+ for each task in topo order:
174
+ if inputs changed (checking actual file mtimes):
175
+ execute task
176
+ update state
177
+ else:
178
+ skip task
179
+ ```
180
+
181
+ The key differences:
182
+ 1. No separate planning phase that checks `dep_statuses`
183
+ 2. Each task checks only the current runtime state of its inputs
184
+ 3. Execution happens immediately when needed, not in a separate loop
185
+ 4. State updates happen immediately after execution
186
+
187
+ ## Implementation Considerations
188
+
189
+ - The `TaskStatus` dataclass and planning phase may need significant restructuring
190
+ - The `check_task_status` method should not receive or examine `dep_statuses`
191
+ - Input checking should always use current file system state, never cached planning decisions
192
+ - The execution order still requires topological sorting (dependencies run before dependents)
193
+ - Edge cases to preserve:
194
+ - Tasks with no inputs/outputs still run every time
195
+ - `--force` flag behaviour
196
+ - Missing outputs trigger re-execution
197
+ - First-run (no cached state) behaviour
198
+
199
+ ## Test Validation
200
+
201
+ The test case should pass after the fix:
202
+ - First run: both tasks execute (establishing baseline)
203
+ - Second run: build runs (no inputs/outputs), package skips (inputs unchanged)
204
+ - The test explicitly verifies build-artifact.txt mtime is unchanged
205
+ - The test asserts package task does NOT run
206
+
207
+ ## Related Code Locations
208
+
209
+ - `src/tasktree/executor.py`: `Executor.execute_task()` method
210
+ - `src/tasktree/executor.py`: `Executor.check_task_status()` method
211
+ - Focus on the dependency triggering logic and execution flow
212
+
213
+ ## Additional Context
214
+
215
+ This bug fundamentally conflicts with the tool's purpose: intelligent incremental execution. Build tools like cargo, make, and cmake have sophisticated internal dependency tracking. When tt wraps these tools, it should respect their decisions about whether work is needed, not override them with static graph analysis.
216
+
217
+ The current implementation would cause spurious rebuilds in common workflows:
218
+ - `cargo build` (checks Rust dependencies, may do nothing)
219
+ - `make` (checks C/C++ timestamps, may do nothing)
220
+ - `cmake --build` (checks build graph, may do nothing)
221
+
222
+ These tools are designed to be incremental. Forcing downstream tasks to run when these tools decide nothing changed defeats the purpose of incremental builds.
@@ -40,7 +40,7 @@
40
40
  "properties": {
41
41
  "shell": {
42
42
  "type": "string",
43
- "description": "Path to the shell executable (e.g., /bin/bash, python3, pwsh)"
43
+ "description": "Path to the shell executable (e.g., /bin/bash, python3, pwsh). Required for shell environments, optional for Docker environments (defaults to 'sh' inside container)."
44
44
  },
45
45
  "args": {
46
46
  "description": "Arguments to pass to the shell",
@@ -59,9 +59,41 @@
59
59
  "preamble": {
60
60
  "type": "string",
61
61
  "description": "Code to execute before the command (e.g., imports, setup)"
62
+ },
63
+ "dockerfile": {
64
+ "type": "string",
65
+ "description": "Path to Dockerfile (relative to recipe file). When present, indicates a Docker environment."
66
+ },
67
+ "context": {
68
+ "type": "string",
69
+ "description": "Path to Docker build context directory (relative to recipe file). Required if dockerfile is specified."
70
+ },
71
+ "volumes": {
72
+ "description": "Volume mounts for Docker container (format: 'host_path:container_path')",
73
+ "type": "array",
74
+ "items": {
75
+ "type": "string"
76
+ }
77
+ },
78
+ "ports": {
79
+ "description": "Port mappings for Docker container (format: 'host_port:container_port')",
80
+ "type": "array",
81
+ "items": {
82
+ "type": "string"
83
+ }
84
+ },
85
+ "env_vars": {
86
+ "description": "Environment variables to set inside the Docker container",
87
+ "type": "object",
88
+ "additionalProperties": {
89
+ "type": "string"
90
+ }
91
+ },
92
+ "working_dir": {
93
+ "type": "string",
94
+ "description": "Working directory inside the container (for Docker environments) or on the host (for shell environments)"
62
95
  }
63
96
  },
64
- "required": ["shell"],
65
97
  "additionalProperties": false
66
98
  }
67
99
  },