tasktree 0.0.22__py3-none-any.whl → 0.0.23__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/docker.py CHANGED
@@ -21,6 +21,7 @@ except ImportError:
21
21
 
22
22
  if TYPE_CHECKING:
23
23
  from tasktree.parser import Environment
24
+ from tasktree.process_runner import ProcessRunner
24
25
 
25
26
 
26
27
  class DockerError(Exception):
@@ -35,7 +36,7 @@ class DockerError(Exception):
35
36
  class DockerManager:
36
37
  """
37
38
  Manages Docker image building and container execution.
38
- @athena: 1a8a919eb05d
39
+ @athena: f8f5c2693d84
39
40
  """
40
41
 
41
42
  def __init__(self, project_root: Path):
@@ -70,12 +71,15 @@ class DockerManager:
70
71
  # Check if os.getuid() and os.getgid() are available (Linux/macOS)
71
72
  return hasattr(os, "getuid") and hasattr(os, "getgid")
72
73
 
73
- def ensure_image_built(self, env: Environment) -> tuple[str, str]:
74
+ def ensure_image_built(
75
+ self, env: Environment, process_runner: ProcessRunner
76
+ ) -> tuple[str, str]:
74
77
  """
75
78
  Build Docker image if not already built this invocation.
76
79
 
77
80
  Args:
78
81
  env: Environment definition with dockerfile and context
82
+ process_runner: ProcessRunner instance for subprocess execution
79
83
 
80
84
  Returns:
81
85
  Tuple of (image_tag, image_id)
@@ -84,7 +88,7 @@ class DockerManager:
84
88
 
85
89
  Raises:
86
90
  DockerError: If docker command not available or build fails
87
- @athena: 9b3c11c29fbb
91
+ @athena: 42b53d2685e0
88
92
  """
89
93
  # Check if already built this invocation
90
94
  if env.name in self._built_images:
@@ -119,7 +123,7 @@ class DockerManager:
119
123
 
120
124
  docker_build_cmd.append(str(context_path))
121
125
 
122
- subprocess.run(
126
+ process_runner.run(
123
127
  docker_build_cmd,
124
128
  check=True,
125
129
  capture_output=False, # Show build output to user
@@ -147,6 +151,7 @@ class DockerManager:
147
151
  cmd: str,
148
152
  working_dir: Path,
149
153
  container_working_dir: str,
154
+ process_runner: ProcessRunner,
150
155
  ) -> subprocess.CompletedProcess:
151
156
  """
152
157
  Execute command inside Docker container.
@@ -156,16 +161,17 @@ class DockerManager:
156
161
  cmd: Command to execute
157
162
  working_dir: Host working directory (for resolving relative volume paths)
158
163
  container_working_dir: Working directory inside container
164
+ process_runner: ProcessRunner instance to use for subprocess execution
159
165
 
160
166
  Returns:
161
167
  CompletedProcess from subprocess.run
162
168
 
163
169
  Raises:
164
170
  DockerError: If docker run fails
165
- @athena: f24fc9c27f81
171
+ @athena: ef024ea2c182
166
172
  """
167
173
  # Ensure image is built (returns tag and ID)
168
- image_tag, image_id = self.ensure_image_built(env)
174
+ image_tag, image_id = self.ensure_image_built(env, process_runner)
169
175
 
170
176
  # Build docker run command
171
177
  docker_cmd = ["docker", "run", "--rm"]
@@ -208,7 +214,7 @@ class DockerManager:
208
214
 
209
215
  # Execute
210
216
  try:
211
- result = subprocess.run(
217
+ result = process_runner.run(
212
218
  docker_cmd,
213
219
  cwd=working_dir,
214
220
  check=True,
@@ -361,7 +367,7 @@ def parse_dockerignore(dockerignore_path: Path) -> PathSpec | None:
361
367
 
362
368
  Returns:
363
369
  PathSpec object for matching, or None if file doesn't exist or pathspec not available
364
- @athena: 62bc07a3c6d0
370
+ @athena: 13fca9ee5a73
365
371
  """
366
372
  if pathspec is None:
367
373
  # pathspec library not available - can't parse .dockerignore
tasktree/executor.py CHANGED
@@ -2,16 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import io
5
6
  import os
6
7
  import platform
7
8
  import stat
8
9
  import subprocess
10
+ import sys
9
11
  import tempfile
10
12
  import time
11
13
  from dataclasses import dataclass, field
12
14
  from datetime import datetime, timezone
13
15
  from pathlib import Path
14
- from typing import Any
16
+ from typing import Any, Callable
15
17
 
16
18
  from tasktree import docker as docker_module
17
19
  from tasktree.graph import (
@@ -21,7 +23,9 @@ from tasktree.graph import (
21
23
  resolve_self_references,
22
24
  )
23
25
  from tasktree.hasher import hash_args, hash_task, make_cache_key
26
+ from tasktree.logging import Logger, LogLevel
24
27
  from tasktree.parser import Recipe, Task, Environment
28
+ from tasktree.process_runner import ProcessRunner, TaskOutputTypes
25
29
  from tasktree.state import StateManager, TaskState
26
30
  from tasktree.hasher import hash_environment_definition
27
31
 
@@ -53,7 +57,7 @@ class ExecutionError(Exception):
53
57
  class Executor:
54
58
  """
55
59
  Executes tasks with incremental execution logic.
56
- @athena: 88e82151721d
60
+ @athena: 779b12944194
57
61
  """
58
62
 
59
63
  # Protected environment variables that cannot be overridden by exported args
@@ -68,17 +72,27 @@ class Executor:
68
72
  "LOGNAME",
69
73
  }
70
74
 
71
- def __init__(self, recipe: Recipe, state_manager: StateManager):
75
+ def __init__(
76
+ self,
77
+ recipe: Recipe,
78
+ state_manager: StateManager,
79
+ logger: Logger,
80
+ process_runner_factory: Callable[[TaskOutputTypes, Logger], ProcessRunner]
81
+ ):
72
82
  """
73
83
  Initialize executor.
74
84
 
75
85
  Args:
76
86
  recipe: Parsed recipe containing all tasks
77
87
  state_manager: State manager for tracking task execution
78
- @athena: 21b65db48bca
88
+ logger_fn: Logger function for output (matches Console.print signature)
89
+ process_runner_factory: Factory function for creating ProcessRunner instances
90
+ @athena: d09e6a537c99
79
91
  """
80
92
  self.recipe = recipe
81
93
  self.state = state_manager
94
+ self.logger = logger
95
+ self._process_runner_factory = process_runner_factory
82
96
  self.docker_manager = docker_module.DockerManager(recipe.project_root)
83
97
 
84
98
  @staticmethod
@@ -91,7 +105,7 @@ class Executor:
91
105
 
92
106
  Returns:
93
107
  True if task has at least one regular (non-exported) argument, False otherwise
94
- @athena: a4c7816bfe61
108
+ @athena: c529cda63cce
95
109
  """
96
110
  if not task.args:
97
111
  return False
@@ -123,7 +137,7 @@ class Executor:
123
137
 
124
138
  Returns:
125
139
  Dictionary containing only regular (non-exported) arguments
126
- @athena: 974e5e32bbd7
140
+ @athena: 1ae863406335
127
141
  """
128
142
  if not task.args or not task_args:
129
143
  return {}
@@ -160,7 +174,7 @@ class Executor:
160
174
 
161
175
  Raises:
162
176
  ExecutionError: If any built-in variable fails to resolve
163
- @athena: 3b4c0ec70ad7
177
+ @athena: a0c1316fd713
164
178
  """
165
179
  import os
166
180
 
@@ -214,7 +228,7 @@ class Executor:
214
228
 
215
229
  Raises:
216
230
  ExecutionError: If any built-in variable fails to resolve
217
- @athena: bb8c385cb0a5
231
+ @athena: 7f6203e8d617
218
232
  """
219
233
  # Get early builtin vars (those that don't depend on working_dir)
220
234
  builtin_vars = self._collect_early_builtin_variables(task, timestamp)
@@ -343,6 +357,7 @@ class Executor:
343
357
  self,
344
358
  task: Task,
345
359
  args_dict: dict[str, Any],
360
+ process_runner: ProcessRunner,
346
361
  force: bool = False,
347
362
  ) -> TaskStatus:
348
363
  """
@@ -361,11 +376,12 @@ class Executor:
361
376
  Args:
362
377
  task: Task to check
363
378
  args_dict: Arguments for this task execution
379
+ process_runner: ProcessRunner instance for subprocess execution
364
380
  force: If True, ignore freshness and force execution
365
381
 
366
382
  Returns:
367
383
  TaskStatus indicating whether task will run and why
368
- @athena: 7252f5db8a4d
384
+ @athena: 03922de1bd23
369
385
  """
370
386
  # If force flag is set, always run
371
387
  if force:
@@ -406,8 +422,9 @@ class Executor:
406
422
  reason="never_run",
407
423
  )
408
424
 
409
- # Check if environment definition has changed
410
- env_changed = self._check_environment_changed(task, cached_state, effective_env)
425
+ env_changed = self._check_environment_changed(
426
+ task, cached_state, effective_env, process_runner
427
+ )
411
428
  if env_changed:
412
429
  return TaskStatus(
413
430
  task_name=task.name,
@@ -446,9 +463,21 @@ class Executor:
446
463
  last_run=datetime.fromtimestamp(cached_state.last_run),
447
464
  )
448
465
 
466
+ @staticmethod
467
+ def _get_task_output_type(user_inputted_value: TaskOutputTypes | None, task: Task) -> TaskOutputTypes:
468
+ if user_inputted_value is None:
469
+ if task.task_output is not None:
470
+ return task.task_output
471
+
472
+ return TaskOutputTypes.ALL
473
+
474
+ return user_inputted_value
475
+
476
+
449
477
  def execute_task(
450
478
  self,
451
479
  task_name: str,
480
+ user_inputted_task_output_types: TaskOutputTypes | None,
452
481
  args_dict: dict[str, Any] | None = None,
453
482
  force: bool = False,
454
483
  only: bool = False,
@@ -458,6 +487,7 @@ class Executor:
458
487
 
459
488
  Args:
460
489
  task_name: Name of task to execute
490
+ task_output_type: TaskOutputTypes enum value for controlling subprocess output
461
491
  args_dict: Arguments to pass to the task
462
492
  force: If True, ignore freshness and re-run all tasks
463
493
  only: If True, run only the specified task without dependencies (implies force=True)
@@ -467,7 +497,7 @@ class Executor:
467
497
 
468
498
  Raises:
469
499
  ExecutionError: If task execution fails
470
- @athena: 1c293ee6a6fa
500
+ @athena: 4773fc590d9a
471
501
  """
472
502
  if args_dict is None:
473
503
  args_dict = {}
@@ -500,8 +530,12 @@ class Executor:
500
530
  # Convert None to {} for internal use (None is used to distinguish simple deps in graph)
501
531
  args_dict_for_execution = task_args if task_args is not None else {}
502
532
 
533
+ process_runner = self._process_runner_factory(self._get_task_output_type(user_inputted_task_output_types, task), self.logger)
534
+
503
535
  # Check if task needs to run (based on CURRENT filesystem state)
504
- status = self.check_task_status(task, args_dict_for_execution, force=force)
536
+ status = self.check_task_status(
537
+ task, args_dict_for_execution, process_runner, force=force
538
+ )
505
539
 
506
540
  # Use a key that includes args for status tracking
507
541
  # Only include regular (non-exported) args in status key for parameterized dependencies
@@ -532,28 +566,29 @@ class Executor:
532
566
  if status.will_run:
533
567
  # Warn if re-running due to missing outputs
534
568
  if status.reason == "outputs_missing":
535
- import sys
536
-
537
- print(
569
+ self.logger.log(
570
+ LogLevel.WARN,
538
571
  f"Warning: Re-running task '{name}' because declared outputs are missing",
539
- file=sys.stderr,
540
572
  )
541
573
 
542
- self._run_task(task, args_dict_for_execution)
574
+ self._run_task(task, args_dict_for_execution, process_runner)
543
575
 
544
576
  return statuses
545
577
 
546
- def _run_task(self, task: Task, args_dict: dict[str, Any]) -> None:
578
+ def _run_task(
579
+ self, task: Task, args_dict: dict[str, Any], process_runner: ProcessRunner
580
+ ) -> None:
547
581
  """
548
582
  Execute a single task.
549
583
 
550
584
  Args:
551
585
  task: Task to execute
552
586
  args_dict: Arguments to substitute in command
587
+ process_runner: ProcessRunner instance for subprocess execution
553
588
 
554
589
  Raises:
555
590
  ExecutionError: If task execution fails
556
- @athena: 4b49652a7afd
591
+ @athena: b5abffeef10a
557
592
  """
558
593
  # Capture timestamp at task start for consistency (in UTC)
559
594
  task_start_time = datetime.now(timezone.utc)
@@ -611,17 +646,30 @@ class Executor:
611
646
  env = self.recipe.get_environment(env_name)
612
647
 
613
648
  # Execute command
614
- print(f"Running: {task.name}")
649
+ self.logger.log(LogLevel.INFO, f"Running: {task.name}")
615
650
 
616
651
  # Route to Docker execution or regular execution
617
652
  if env and env.dockerfile:
618
653
  # Docker execution path
619
- self._run_task_in_docker(task, env, cmd, working_dir, exported_env_vars)
654
+ self._run_task_in_docker(
655
+ task,
656
+ env,
657
+ cmd,
658
+ working_dir,
659
+ process_runner,
660
+ exported_env_vars,
661
+ )
620
662
  else:
621
663
  # Regular execution path - use unified script-based execution
622
664
  shell, preamble = self._resolve_environment(task)
623
665
  self._run_command_as_script(
624
- cmd, working_dir, task.name, shell, preamble, exported_env_vars
666
+ cmd,
667
+ working_dir,
668
+ task.name,
669
+ shell,
670
+ preamble,
671
+ process_runner,
672
+ exported_env_vars,
625
673
  )
626
674
 
627
675
  # Update state
@@ -634,6 +682,7 @@ class Executor:
634
682
  task_name: str,
635
683
  shell: str,
636
684
  preamble: str,
685
+ process_runner: ProcessRunner,
637
686
  exported_env_vars: dict[str, str] | None = None,
638
687
  ) -> None:
639
688
  """
@@ -649,12 +698,13 @@ class Executor:
649
698
  task_name: Task name (for error messages)
650
699
  shell: Shell to use for script execution
651
700
  preamble: Preamble text to prepend to script
701
+ process_runner: ProcessRunner instance to use for subprocess execution
652
702
  exported_env_vars: Exported arguments to set as environment variables
653
703
 
654
704
  Raises:
655
705
  ExecutionError: If command execution fails
656
706
  @athena: TBD
657
- @athena: 96e85dc15b5c
707
+ @athena: 228cc00e7665
658
708
  """
659
709
  # Prepare environment with exported args
660
710
  env = self._prepare_env_with_exports(exported_env_vars)
@@ -694,13 +744,56 @@ class Executor:
694
744
 
695
745
  # Execute script file
696
746
  try:
697
- subprocess.run(
698
- [script_path],
699
- cwd=working_dir,
700
- check=True,
701
- capture_output=False,
702
- env=env,
703
- )
747
+ # Check if stdout/stderr support fileno() (real file descriptors)
748
+ # CliRunner uses StringIO which has fileno() method but raises when called
749
+ def supports_fileno(stream):
750
+ """Check if a stream has a working fileno() method."""
751
+ try:
752
+ stream.fileno()
753
+ return True
754
+ except (AttributeError, OSError, io.UnsupportedOperation):
755
+ return False
756
+
757
+ # Determine output targets based on task_output mode
758
+ # For "all" mode: show everything
759
+ # Future modes: use subprocess.DEVNULL for suppression
760
+ should_suppress = False # Will be: self.task_output == "none", etc.
761
+
762
+ if should_suppress:
763
+ stdout_target = subprocess.DEVNULL
764
+ stderr_target = subprocess.DEVNULL
765
+ else:
766
+ stdout_target = sys.stdout
767
+ stderr_target = sys.stderr
768
+
769
+ # If streams support fileno, pass target streams directly (most efficient)
770
+ # Otherwise capture and manually write (CliRunner compatibility)
771
+ if not should_suppress and not (
772
+ supports_fileno(sys.stdout) and supports_fileno(sys.stderr)
773
+ ):
774
+ # CliRunner path: capture and write manually
775
+ result = process_runner.run(
776
+ [script_path],
777
+ cwd=working_dir,
778
+ check=True,
779
+ capture_output=True,
780
+ text=True,
781
+ env=env,
782
+ )
783
+ if result.stdout:
784
+ sys.stdout.write(result.stdout)
785
+ if result.stderr:
786
+ sys.stderr.write(result.stderr)
787
+ else:
788
+ # Normal execution path: use target streams (including DEVNULL when suppressing)
789
+ process_runner.run(
790
+ [script_path],
791
+ cwd=working_dir,
792
+ check=True,
793
+ stdout=stdout_target,
794
+ stderr=stderr_target,
795
+ env=env,
796
+ )
704
797
  except subprocess.CalledProcessError as e:
705
798
  raise ExecutionError(
706
799
  f"Task '{task_name}' failed with exit code {e.returncode}"
@@ -727,7 +820,7 @@ class Executor:
727
820
 
728
821
  Raises:
729
822
  ValueError: If builtin variable or environment variable is not defined
730
- @athena: 21e2ccd27dbb
823
+ @athena: eba6e3d62062
731
824
  """
732
825
  from dataclasses import replace
733
826
 
@@ -798,6 +891,7 @@ class Executor:
798
891
  env: Any,
799
892
  cmd: str,
800
893
  working_dir: Path,
894
+ process_runner: ProcessRunner,
801
895
  exported_env_vars: dict[str, str] | None = None,
802
896
  ) -> None:
803
897
  """
@@ -808,11 +902,13 @@ class Executor:
808
902
  env: Docker environment configuration
809
903
  cmd: Command to execute
810
904
  working_dir: Host working directory
905
+ process_runner: ProcessRunner instance to use for subprocess execution
811
906
  exported_env_vars: Exported arguments to set as environment variables
907
+ task_output: Control task subprocess output (all, out, err, on-err, none)
812
908
 
813
909
  Raises:
814
910
  ExecutionError: If Docker execution fails
815
- @athena: fe972e4c97a3
911
+ @athena: 61725a57e304
816
912
  """
817
913
  # Get builtin variables for substitution in environment fields
818
914
  task_start_time = datetime.now(timezone.utc)
@@ -852,6 +948,7 @@ class Executor:
852
948
  cmd=cmd,
853
949
  working_dir=working_dir,
854
950
  container_working_dir=container_working_dir,
951
+ process_runner=process_runner,
855
952
  )
856
953
  except docker_module.DockerError as e:
857
954
  raise ExecutionError(str(e)) from e
@@ -868,7 +965,7 @@ class Executor:
868
965
 
869
966
  Raises:
870
967
  ExecutionError: If {{ tt.working_dir }} placeholder is found
871
- @athena: 617a0c609f4d
968
+ @athena: 82822f02716a
872
969
  """
873
970
  import re
874
971
 
@@ -977,7 +1074,11 @@ class Executor:
977
1074
 
978
1075
  # TODO: Understand why task isn't used
979
1076
  def _check_environment_changed(
980
- self, task: Task, cached_state: TaskState, env_name: str
1077
+ self,
1078
+ task: Task,
1079
+ cached_state: TaskState,
1080
+ env_name: str,
1081
+ process_runner: ProcessRunner,
981
1082
  ) -> bool:
982
1083
  """
983
1084
  Check if environment definition has changed since last run.
@@ -989,10 +1090,11 @@ class Executor:
989
1090
  task: Task to check
990
1091
  cached_state: Cached state from previous run
991
1092
  env_name: Effective environment name (from _get_effective_env_name)
1093
+ process_runner: ProcessRunner instance for subprocess execution
992
1094
 
993
1095
  Returns:
994
1096
  True if environment definition changed, False otherwise
995
- @athena: 052561b75455
1097
+ @athena: e206e104150a
996
1098
  """
997
1099
  # If using platform default (no environment), no definition to track
998
1100
  if not env_name:
@@ -1023,13 +1125,19 @@ class Executor:
1023
1125
 
1024
1126
  # For Docker environments, also check if image ID changed
1025
1127
  if env.dockerfile:
1026
- return self._check_docker_image_changed(env, cached_state, env_name)
1128
+ return self._check_docker_image_changed(
1129
+ env, cached_state, env_name, process_runner
1130
+ )
1027
1131
 
1028
1132
  # Shell environment with unchanged hash
1029
1133
  return False
1030
1134
 
1031
1135
  def _check_docker_image_changed(
1032
- self, env: Environment, cached_state: TaskState, env_name: str
1136
+ self,
1137
+ env: Environment,
1138
+ cached_state: TaskState,
1139
+ env_name: str,
1140
+ process_runner: ProcessRunner,
1033
1141
  ) -> bool:
1034
1142
  """
1035
1143
  Check if Docker image ID has changed.
@@ -1041,14 +1149,17 @@ class Executor:
1041
1149
  env: Docker environment definition
1042
1150
  cached_state: Cached state from previous run
1043
1151
  env_name: Environment name
1152
+ process_runner: ProcessRunner instance for subprocess execution
1044
1153
 
1045
1154
  Returns:
1046
1155
  True if image ID changed, False otherwise
1047
- @athena: 0443710cf356
1156
+ @athena: bc954288e4ad
1048
1157
  """
1049
1158
  # Build/ensure image is built and get its ID
1050
1159
  try:
1051
- image_tag, current_image_id = self.docker_manager.ensure_image_built(env)
1160
+ image_tag, current_image_id = self.docker_manager.ensure_image_built(
1161
+ env, process_runner
1162
+ )
1052
1163
  except Exception:
1053
1164
  # If we can't build, treat as changed (will fail later with better error)
1054
1165
  return True
@@ -1243,7 +1354,7 @@ class Executor:
1243
1354
  def _update_state(self, task: Task, args_dict: dict[str, Any]) -> None:
1244
1355
  """
1245
1356
  Update state after task execution.
1246
- @athena: 1fcfdfcb9be9
1357
+ @athena: f4d3efdaac7c
1247
1358
  """
1248
1359
  cache_key = self._cache_key(task, args_dict)
1249
1360
  input_state = self._input_files_to_modified_times(task)
@@ -1261,6 +1372,9 @@ class Executor:
1261
1372
  self.state.save()
1262
1373
 
1263
1374
  def _cache_key(self, task: Task, args_dict: dict[str, Any]) -> str:
1375
+ """
1376
+ @athena: d20ce4090741
1377
+ """
1264
1378
  effective_env = self._get_effective_env_name(task)
1265
1379
  task_hash = hash_task(
1266
1380
  task.cmd,
@@ -1274,6 +1388,9 @@ class Executor:
1274
1388
  return make_cache_key(task_hash, args_hash)
1275
1389
 
1276
1390
  def _input_files_to_modified_times(self, task: Task) -> dict[str, float]:
1391
+ """
1392
+ @athena: 7e5ba779a41f
1393
+ """
1277
1394
  input_files = self._expand_globs(self._get_all_inputs(task), task.working_dir)
1278
1395
 
1279
1396
  input_state = {}
@@ -1291,6 +1408,9 @@ class Executor:
1291
1408
  def _docker_inputs_to_modified_times(
1292
1409
  self, env_name: str, env: Environment
1293
1410
  ) -> dict[str, float]:
1411
+ """
1412
+ @athena: bfe53b0d56cd
1413
+ """
1294
1414
  input_state = dict()
1295
1415
  # Record Dockerfile mtime
1296
1416
  dockerfile_path = self.recipe.project_root / env.dockerfile
tasktree/graph.py CHANGED
@@ -98,7 +98,7 @@ def resolve_dependency_invocation(
98
98
  ... recipe
99
99
  ... )
100
100
  DependencyInvocation("build", {"mode": "production"})
101
- @athena: 968bae796809
101
+ @athena: dd9a84821159
102
102
  """
103
103
  # Simple string case - no args to substitute
104
104
  if isinstance(dep_spec, str):
@@ -260,7 +260,7 @@ def resolve_execution_order(
260
260
  Raises:
261
261
  TaskNotFoundError: If target task or any dependency doesn't exist
262
262
  CycleError: If a dependency cycle is detected
263
- @athena: b4443e1cb45d
263
+ @athena: 687f627efc75
264
264
  """
265
265
  if target_task not in recipe.tasks:
266
266
  raise TaskNotFoundError(f"Task not found: {target_task}")
@@ -607,7 +607,7 @@ def build_dependency_tree(
607
607
 
608
608
  Returns:
609
609
  Nested dictionary representing the dependency tree
610
- @athena: 570e5c663887
610
+ @athena: 8c853b393bdb
611
611
  """
612
612
  if target_task not in recipe.tasks:
613
613
  raise TaskNotFoundError(f"Task not found: {target_task}")
tasktree/hasher.py CHANGED
@@ -35,12 +35,12 @@ def _normalize_choices_lists(
35
35
  Normalize argument choices lists by sorting them for deterministic hashing.
36
36
 
37
37
  Args:
38
- args: List of argument specifications (strings or dicts)
38
+ args: List of argument specifications (strings or dicts)
39
39
 
40
40
  Returns:
41
- List of argument specs with sorted choices lists
41
+ List of argument specs with sorted choices lists
42
42
 
43
- @athena: 7512379275e3
43
+ @athena: dc34b6fb09a6
44
44
  """
45
45
  normalized_args = []
46
46
  for arg in args:
@@ -114,7 +114,7 @@ def hash_task(
114
114
 
115
115
  Returns:
116
116
  8-character hash of task definition
117
- @athena: 7a461d51a8bb
117
+ @athena: 815a47cf1af0
118
118
  """
119
119
  data = {
120
120
  "cmd": cmd,
@@ -173,7 +173,7 @@ def hash_environment_definition(env) -> str:
173
173
 
174
174
  Returns:
175
175
  16-character hash of environment definition
176
- @athena: 2de34f1a0b4a
176
+ @athena: a54f6c171ba9
177
177
  """
178
178
  # Import inside function to avoid circular dependency
179
179