tasktree 0.0.10__tar.gz → 0.0.11__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 (73) hide show
  1. {tasktree-0.0.10 → tasktree-0.0.11}/PKG-INFO +4 -2
  2. {tasktree-0.0.10 → tasktree-0.0.11}/README.md +3 -1
  3. {tasktree-0.0.10 → tasktree-0.0.11}/pyproject.toml +1 -1
  4. {tasktree-0.0.10 → tasktree-0.0.11}/schema/tasktree-schema.json +66 -1
  5. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/cli.py +5 -3
  6. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/parser.py +24 -27
  7. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_end_to_end.py +5 -1
  8. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_exported_args.py +24 -6
  9. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_parameterized_dependencies.yaml +2 -2
  10. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_parameterized_deps_execution.py +4 -4
  11. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_cli.py +4 -4
  12. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_dependency_parsing.py +1 -1
  13. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_list_formatting.py +9 -9
  14. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_parameterized_graph.py +1 -1
  15. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_parser.py +35 -40
  16. {tasktree-0.0.10 → tasktree-0.0.11}/.claude/settings.local.json +0 -0
  17. {tasktree-0.0.10 → tasktree-0.0.11}/.github/workflows/claude-code-review.yml +0 -0
  18. {tasktree-0.0.10 → tasktree-0.0.11}/.github/workflows/claude.yml +0 -0
  19. {tasktree-0.0.10 → tasktree-0.0.11}/.github/workflows/release.yml +0 -0
  20. {tasktree-0.0.10 → tasktree-0.0.11}/.github/workflows/test.yml +0 -0
  21. {tasktree-0.0.10 → tasktree-0.0.11}/.gitignore +0 -0
  22. {tasktree-0.0.10 → tasktree-0.0.11}/.python-version +0 -0
  23. {tasktree-0.0.10 → tasktree-0.0.11}/CLAUDE.md +0 -0
  24. {tasktree-0.0.10 → tasktree-0.0.11}/example/source.txt +0 -0
  25. {tasktree-0.0.10 → tasktree-0.0.11}/example/tasktree.yaml +0 -0
  26. {tasktree-0.0.10 → tasktree-0.0.11}/requirements/implemented/01-basic-variables.md +0 -0
  27. {tasktree-0.0.10 → tasktree-0.0.11}/requirements/implemented/02-env-variable-type.md +0 -0
  28. {tasktree-0.0.10 → tasktree-0.0.11}/requirements/implemented/03-direct-env-substitution.md +0 -0
  29. {tasktree-0.0.10 → tasktree-0.0.11}/requirements/implemented/04-file-read-variables.md +0 -0
  30. {tasktree-0.0.10 → tasktree-0.0.11}/requirements/implemented/bug-report-dependency-triggering.md +0 -0
  31. {tasktree-0.0.10 → tasktree-0.0.11}/requirements/implemented/docker-task-environments.md +0 -0
  32. {tasktree-0.0.10 → tasktree-0.0.11}/requirements/implemented/shell-environment-requirements.md +0 -0
  33. {tasktree-0.0.10 → tasktree-0.0.11}/schema/README.md +0 -0
  34. {tasktree-0.0.10 → tasktree-0.0.11}/schema/vscode-settings-snippet.json +0 -0
  35. {tasktree-0.0.10 → tasktree-0.0.11}/src/__init__.py +0 -0
  36. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/__init__.py +0 -0
  37. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/docker.py +0 -0
  38. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/executor.py +0 -0
  39. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/graph.py +0 -0
  40. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/hasher.py +0 -0
  41. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/state.py +0 -0
  42. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/substitution.py +0 -0
  43. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/tasks.py +0 -0
  44. {tasktree-0.0.10 → tasktree-0.0.11}/src/tasktree/types.py +0 -0
  45. {tasktree-0.0.10 → tasktree-0.0.11}/tasktree.yaml +0 -0
  46. {tasktree-0.0.10 → tasktree-0.0.11}/tests/e2e/__init__.py +0 -0
  47. {tasktree-0.0.10 → tasktree-0.0.11}/tests/e2e/test_docker_basic.py +0 -0
  48. {tasktree-0.0.10 → tasktree-0.0.11}/tests/e2e/test_docker_environment.py +0 -0
  49. {tasktree-0.0.10 → tasktree-0.0.11}/tests/e2e/test_docker_ownership.py +0 -0
  50. {tasktree-0.0.10 → tasktree-0.0.11}/tests/e2e/test_docker_volumes.py +0 -0
  51. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_arg_choices.py +0 -0
  52. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_arg_min_max.py +0 -0
  53. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_builtin_variables.py +0 -0
  54. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_clean_state.py +0 -0
  55. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_cli_options.py +0 -0
  56. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_dependency_execution.py +0 -0
  57. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_docker_build_args.py +0 -0
  58. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_input_detection.py +0 -0
  59. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_missing_outputs.py +0 -0
  60. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_nested_imports.py +0 -0
  61. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_state_persistence.py +0 -0
  62. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_variables.py +0 -0
  63. {tasktree-0.0.10 → tasktree-0.0.11}/tests/integration/test_working_directory.py +0 -0
  64. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_docker.py +0 -0
  65. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_environment_tracking.py +0 -0
  66. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_executor.py +0 -0
  67. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_graph.py +0 -0
  68. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_hasher.py +0 -0
  69. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_state.py +0 -0
  70. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_substitution.py +0 -0
  71. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_tasks.py +0 -0
  72. {tasktree-0.0.10 → tasktree-0.0.11}/tests/unit/test_types.py +0 -0
  73. {tasktree-0.0.10 → tasktree-0.0.11}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.10
3
+ Version: 0.0.11
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
@@ -604,7 +604,9 @@ Dependencies can invoke tasks with specific arguments, enabling flexible and reu
604
604
  tasks:
605
605
  # Task with parameters
606
606
  process:
607
- args: [mode, verbose=false]
607
+ args:
608
+ - mode
609
+ - verbose: { default: false }
608
610
  cmd: echo "mode={{arg.mode}} verbose={{arg.verbose}}"
609
611
 
610
612
  # Simple dependency (uses defaults)
@@ -589,7 +589,9 @@ Dependencies can invoke tasks with specific arguments, enabling flexible and reu
589
589
  tasks:
590
590
  # Task with parameters
591
591
  process:
592
- args: [mode, verbose=false]
592
+ args:
593
+ - mode
594
+ - verbose: { default: false }
593
595
  cmd: echo "mode={{arg.mode}} verbose={{arg.verbose}}"
594
596
 
595
597
  # Simple dependency (uses defaults)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tasktree"
3
- version = "0.0.10"
3
+ version = "0.0.11"
4
4
  description = "A task automation tool with incremental execution"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -99,6 +99,70 @@
99
99
  },
100
100
  "additionalProperties": false
101
101
  },
102
+ "variables": {
103
+ "description": "Variable definitions that can be referenced in tasks using {{ var.name }} syntax",
104
+ "type": "object",
105
+ "patternProperties": {
106
+ "^[a-zA-Z][a-zA-Z0-9_-]*$": {
107
+ "oneOf": [
108
+ {
109
+ "type": "string",
110
+ "description": "Simple string value"
111
+ },
112
+ {
113
+ "type": "integer",
114
+ "description": "Integer value"
115
+ },
116
+ {
117
+ "type": "number",
118
+ "description": "Floating-point number value"
119
+ },
120
+ {
121
+ "type": "boolean",
122
+ "description": "Boolean value (true/false)"
123
+ },
124
+ {
125
+ "type": "object",
126
+ "description": "Environment variable reference",
127
+ "properties": {
128
+ "env": {
129
+ "type": "string",
130
+ "description": "Name of environment variable to read",
131
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
132
+ }
133
+ },
134
+ "required": ["env"],
135
+ "additionalProperties": false
136
+ },
137
+ {
138
+ "type": "object",
139
+ "description": "File read reference",
140
+ "properties": {
141
+ "read": {
142
+ "type": "string",
143
+ "description": "Path to file to read (relative to recipe file, or absolute)"
144
+ }
145
+ },
146
+ "required": ["read"],
147
+ "additionalProperties": false
148
+ },
149
+ {
150
+ "type": "object",
151
+ "description": "Eval command reference (executes shell command and captures output)",
152
+ "properties": {
153
+ "eval": {
154
+ "type": "string",
155
+ "description": "Shell command to execute (stdout becomes variable value)"
156
+ }
157
+ },
158
+ "required": ["eval"],
159
+ "additionalProperties": false
160
+ }
161
+ ]
162
+ }
163
+ },
164
+ "additionalProperties": false
165
+ },
102
166
  "tasks": {
103
167
  "description": "Task definitions",
104
168
  "type": "object",
@@ -190,6 +254,7 @@
190
254
  "anyOf": [
191
255
  { "required": ["tasks"] },
192
256
  { "required": ["imports"] },
193
- { "required": ["environments"] }
257
+ { "required": ["environments"] },
258
+ { "required": ["variables"] }
194
259
  ]
195
260
  }
@@ -192,10 +192,12 @@ tasks:
192
192
  # deploy:
193
193
  # desc: Deploy to environment
194
194
  # deps: [build]
195
- # args: [environment, region=eu-west-1]
195
+ # args:
196
+ # - environment
197
+ # - region: { default: eu-west-1 }
196
198
  # cmd: |
197
- # echo "Deploying to {{environment}} in {{region}}"
198
- # ./deploy.sh {{environment}} {{region}}
199
+ # echo "Deploying to {{ arg.environment }} in {{ arg.region }}"
200
+ # ./deploy.sh {{ arg.environment }} {{ arg.region }}
199
201
 
200
202
  # Uncomment and modify the examples above to define your tasks
201
203
  """
@@ -1259,18 +1259,16 @@ def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
1259
1259
 
1260
1260
  Supports both string format and dictionary format:
1261
1261
 
1262
- String format:
1262
+ String format (simple names only):
1263
1263
  - Simple name: "argname"
1264
1264
  - Exported (becomes env var): "$argname"
1265
- - With default: "argname=value" or "$argname=value"
1266
- - Legacy type syntax: "argname:type=value" (for backwards compat)
1267
1265
 
1268
1266
  Dictionary format:
1269
1267
  - argname: { default: "value" }
1270
1268
  - argname: { type: int, default: 42 }
1271
1269
  - argname: { type: int, min: 1, max: 100 }
1272
1270
  - argname: { type: str, choices: ["dev", "staging", "prod"] }
1273
- - $argname: { default: "value" } # Exported
1271
+ - $argname: { default: "value" } # Exported (type not allowed)
1274
1272
 
1275
1273
  Args:
1276
1274
  arg_spec: Argument specification (string or dict with single key)
@@ -1328,34 +1326,24 @@ def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
1328
1326
  if is_exported:
1329
1327
  arg_spec = arg_spec[1:] # Remove $ prefix
1330
1328
 
1331
- # Split on = to separate name:type from default
1332
- if "=" in arg_spec:
1333
- name_type, default = arg_spec.split("=", 1)
1334
- else:
1335
- name_type = arg_spec
1336
- default = None
1329
+ # String format only supports simple names (no = or :)
1330
+ if "=" in arg_spec or ":" in arg_spec:
1331
+ raise ValueError(
1332
+ f"Invalid argument syntax: {'$' if is_exported else ''}{arg_spec}\n\n"
1333
+ f"String format only supports simple argument names.\n"
1334
+ f"Use YAML dict format for type annotations, defaults, or constraints:\n"
1335
+ f" args:\n"
1336
+ f" - {'$' if is_exported else ''}{arg_spec.split('=')[0].split(':')[0]}: {{ default: value }}"
1337
+ )
1337
1338
 
1338
- # Split on : to separate name from type
1339
- if ":" in name_type:
1340
- name, arg_type = name_type.split(":", 1)
1339
+ name = arg_spec
1340
+ arg_type = "str"
1341
1341
 
1342
- # Exported arguments cannot have type annotations
1343
- if is_exported:
1344
- raise ValueError(
1345
- f"Type annotations not allowed on exported arguments\n"
1346
- f"In argument: ${name}:{arg_type}\n\n"
1347
- f"Exported arguments are always strings. Remove the type annotation:\n"
1348
- f" args: [${name}]"
1349
- )
1350
- else:
1351
- name = name_type
1352
- arg_type = "str"
1353
-
1354
- # String format doesn't support min/max/choices
1342
+ # String format doesn't support min/max/choices/defaults
1355
1343
  return ArgSpec(
1356
1344
  name=name,
1357
1345
  arg_type=arg_type,
1358
- default=default,
1346
+ default=None,
1359
1347
  is_exported=is_exported,
1360
1348
  min_val=None,
1361
1349
  max_val=None,
@@ -1403,6 +1391,15 @@ def _parse_arg_dict(arg_name: str, config: dict, is_exported: bool) -> ArgSpec:
1403
1391
  f"Exported arguments are always strings. Remove the 'type' field"
1404
1392
  )
1405
1393
 
1394
+ # Exported arguments must have string defaults (if any default is provided)
1395
+ if is_exported and default is not None and not isinstance(default, str):
1396
+ raise ValueError(
1397
+ f"Exported argument '${arg_name}' must have a string default value.\n"
1398
+ f"Got: {default!r} (type: {type(default).__name__})\n"
1399
+ f"Exported arguments become environment variables, which are always strings.\n"
1400
+ f"Use a quoted string: ${arg_name}: {{ default: \"{default}\" }}"
1401
+ )
1402
+
1406
1403
  # Validate choices
1407
1404
  if choices is not None:
1408
1405
  # Validate choices is a list
@@ -35,7 +35,11 @@ class TestEndToEnd(unittest.TestCase):
35
35
  recipe_file.write_text("""
36
36
  tasks:
37
37
  deploy:
38
- args: [environment, region:str=us-west-1, port:int=8080, debug:bool=false]
38
+ args:
39
+ - environment
40
+ - region: { type: str, default: us-west-1 }
41
+ - port: { type: int, default: 8080 }
42
+ - debug: { type: bool, default: false }
39
43
  outputs: [deploy.log]
40
44
  cmd: echo "env={{ arg.environment }} region={{ arg.region }} port={{ arg.port }} debug={{ arg.debug }}" > deploy.log
41
45
  """)
@@ -28,7 +28,9 @@ class TestExportedArgs(unittest.TestCase):
28
28
  f"""
29
29
  tasks:
30
30
  test:
31
- args: [$server, $user=admin]
31
+ args:
32
+ - $server
33
+ - $user: {{ default: admin }}
32
34
  cmd: {env_check}
33
35
  """
34
36
  )
@@ -59,7 +61,9 @@ tasks:
59
61
  f"""
60
62
  tasks:
61
63
  test:
62
- args: [$server, $port=8080]
64
+ args:
65
+ - $server
66
+ - $port: {{ default: "8080" }}
63
67
  cmd: {env_check}
64
68
  """
65
69
  )
@@ -94,7 +98,9 @@ tasks:
94
98
  f"""
95
99
  tasks:
96
100
  test:
97
- args: [$server, $port=8080]
101
+ args:
102
+ - $server
103
+ - $port: {{ default: "8080" }}
98
104
  cmd: {env_check}
99
105
  """
100
106
  )
@@ -164,7 +170,9 @@ tasks:
164
170
  f"""
165
171
  tasks:
166
172
  deploy:
167
- args: [$server, port=8080]
173
+ args:
174
+ - $server
175
+ - port: {{ default: 8080 }}
168
176
  cmd: {cmd_line}
169
177
  """
170
178
  )
@@ -242,12 +250,22 @@ tasks:
242
250
  """Test that exported arguments with type annotations fail during arg parsing."""
243
251
  from tasktree.parser import parse_arg_spec
244
252
 
245
- # Test that parse_arg_spec raises error for exported args with types
253
+ # Test that parse_arg_spec raises error for exported args with types (old colon syntax)
246
254
  with self.assertRaises(ValueError) as cm:
247
255
  parse_arg_spec("$server:str")
248
256
 
257
+ self.assertIn("Invalid argument syntax", str(cm.exception))
258
+
259
+ def test_exported_arg_yaml_dict_with_type_fails(self):
260
+ """Test that exported arguments with type field in YAML dict format fails."""
261
+ from tasktree.parser import parse_arg_spec
262
+
263
+ # Test that parse_arg_spec raises error for exported args with type in dict format
264
+ with self.assertRaises(ValueError) as cm:
265
+ parse_arg_spec({"$server": {"type": "str"}})
266
+
249
267
  self.assertIn("Type annotations not allowed", str(cm.exception))
250
- self.assertIn("$server:str", str(cm.exception))
268
+ self.assertIn("$server", str(cm.exception))
251
269
 
252
270
  def test_multiline_command_with_exported_args(self):
253
271
  """Test exported args work with multi-line commands."""
@@ -1,8 +1,8 @@
1
1
  tasks:
2
2
  task_parameterized:
3
3
  args:
4
- - mode=debug
5
- - verbose=false
4
+ - mode: { default: "debug" }
5
+ - verbose: { default: "false" }
6
6
  cmd: echo "mode={{arg.mode}} verbose={{arg.verbose}}"
7
7
 
8
8
  task_consumer_implicit:
@@ -37,8 +37,8 @@ class TestParameterizedDependencyExecution(unittest.TestCase):
37
37
  tasks:
38
38
  build:
39
39
  args:
40
- - mode=debug
41
- - optimize:bool=false
40
+ - mode: { default: "debug" }
41
+ - optimize: { type: bool, default: false }
42
42
  outputs:
43
43
  - "build-{{arg.mode}}.log"
44
44
  cmd: echo "Building {{arg.mode}} optimize={{arg.optimize}}" > build-{{arg.mode}}.log
@@ -151,8 +151,8 @@ tasks:
151
151
  tasks:
152
152
  generate:
153
153
  args:
154
- - format=json
155
- - pretty:bool=false
154
+ - format: { default: "json" }
155
+ - pretty: { type: bool, default: false }
156
156
  outputs:
157
157
  - "data.{{arg.format}}"
158
158
  cmd: echo "{{arg.format}},pretty={{arg.pretty}}" > data.{{arg.format}}
@@ -31,7 +31,7 @@ class TestParseTaskArgs(unittest.TestCase):
31
31
 
32
32
  def test_parse_task_args_with_defaults(self):
33
33
  """Test default values applied."""
34
- arg_specs = ["environment", "region=us-west-1"]
34
+ arg_specs = ["environment", {"region": {"default": "us-west-1"}}]
35
35
  arg_values = ["production"] # Only provide first arg
36
36
 
37
37
  result = _parse_task_args(arg_specs, arg_values)
@@ -40,7 +40,7 @@ class TestParseTaskArgs(unittest.TestCase):
40
40
 
41
41
  def test_parse_task_args_type_conversion(self):
42
42
  """Test values converted to correct types."""
43
- arg_specs = ["port:int", "debug:bool", "timeout:float"]
43
+ arg_specs = [{"port": {"type": "int"}}, {"debug": {"type": "bool"}}, {"timeout": {"type": "float"}}]
44
44
  arg_values = ["8080", "true", "30.5"]
45
45
 
46
46
  result = _parse_task_args(arg_specs, arg_values)
@@ -76,7 +76,7 @@ class TestParseTaskArgs(unittest.TestCase):
76
76
 
77
77
  def test_parse_task_args_invalid_type(self):
78
78
  """Test error for invalid type conversion."""
79
- arg_specs = ["port:int"]
79
+ arg_specs = [{"port": {"type": "int"}}]
80
80
  arg_values = ["not_a_number"]
81
81
 
82
82
  with self.assertRaises(typer.Exit):
@@ -93,7 +93,7 @@ class TestParseTaskArgs(unittest.TestCase):
93
93
 
94
94
  def test_parse_task_args_mixed(self):
95
95
  """Test mixing positional and named arguments."""
96
- arg_specs = ["environment", "region", "verbose:bool"]
96
+ arg_specs = ["environment", "region", {"verbose": {"type": "bool"}}]
97
97
  arg_values = ["production", "region=us-east-1", "verbose=true"]
98
98
 
99
99
  result = _parse_task_args(arg_specs, arg_values)
@@ -18,7 +18,7 @@ class TestDependencyParsing(unittest.TestCase):
18
18
  self.task_with_args = Task(
19
19
  name="process",
20
20
  cmd="echo mode={{arg.mode}} verbose={{arg.verbose}}",
21
- args=["mode", "verbose=false"],
21
+ args=["mode", {"verbose": {"default": "false"}}],
22
22
  )
23
23
  self.task_no_args = Task(
24
24
  name="build",
@@ -25,7 +25,7 @@ class TestFormatTaskArguments(unittest.TestCase):
25
25
 
26
26
  def test_format_single_optional_argument(self):
27
27
  """Test formatting task with single optional argument."""
28
- result = _format_task_arguments(["environment=production"])
28
+ result = _format_task_arguments([{"environment": {"default": "production"}}])
29
29
  self.assertIn("environment[dim]:str[/dim]", result)
30
30
  self.assertIn("[dim]\\[=production][/dim]", result)
31
31
 
@@ -39,7 +39,7 @@ class TestFormatTaskArguments(unittest.TestCase):
39
39
 
40
40
  def test_format_multiple_optional_arguments(self):
41
41
  """Test formatting task with multiple optional arguments."""
42
- result = _format_task_arguments(["mode=debug", "target=x86_64"])
42
+ result = _format_task_arguments([{"mode": {"default": "debug"}}, {"target": {"default": "x86_64"}}])
43
43
  self.assertIn("mode[dim]:str[/dim]", result)
44
44
  self.assertIn("[dim]\\[=debug][/dim]", result)
45
45
  self.assertIn("target[dim]:str[/dim]", result)
@@ -47,7 +47,7 @@ class TestFormatTaskArguments(unittest.TestCase):
47
47
 
48
48
  def test_format_mixed_required_and_optional_arguments(self):
49
49
  """Test formatting task with mixed required and optional arguments."""
50
- result = _format_task_arguments(["environment", "region=us-west-1"])
50
+ result = _format_task_arguments(["environment", {"region": {"default": "us-west-1"}}])
51
51
  self.assertIn("environment[dim]:str[/dim]", result)
52
52
  self.assertIn("region[dim]:str[/dim]", result)
53
53
  self.assertIn("[dim]\\[=us-west-1][/dim]", result)
@@ -66,7 +66,7 @@ class TestFormatTaskArguments(unittest.TestCase):
66
66
 
67
67
  def test_format_default_values_with_equals_sign(self):
68
68
  """Test formatting shows default values with equals sign."""
69
- result = _format_task_arguments(["port=8080"])
69
+ result = _format_task_arguments([{"port": {"default": "8080"}}])
70
70
  self.assertIn("[dim]\\[=8080][/dim]", result)
71
71
 
72
72
  def test_format_shows_str_type_explicitly(self):
@@ -76,22 +76,22 @@ class TestFormatTaskArguments(unittest.TestCase):
76
76
 
77
77
  def test_format_shows_int_type(self):
78
78
  """Test formatting shows int type."""
79
- result = _format_task_arguments(["port:int"])
79
+ result = _format_task_arguments([{"port": {"type": "int"}}])
80
80
  self.assertIn("port[dim]:int[/dim]", result)
81
81
 
82
82
  def test_format_shows_float_type(self):
83
83
  """Test formatting shows float type."""
84
- result = _format_task_arguments(["timeout:float"])
84
+ result = _format_task_arguments([{"timeout": {"type": "float"}}])
85
85
  self.assertIn("timeout[dim]:float[/dim]", result)
86
86
 
87
87
  def test_format_shows_bool_type(self):
88
88
  """Test formatting shows bool type."""
89
- result = _format_task_arguments(["verbose:bool"])
89
+ result = _format_task_arguments([{"verbose": {"type": "bool"}}])
90
90
  self.assertIn("verbose[dim]:bool[/dim]", result)
91
91
 
92
92
  def test_format_shows_path_type(self):
93
93
  """Test formatting shows path type."""
94
- result = _format_task_arguments(["output:path"])
94
+ result = _format_task_arguments([{"output": {"type": "path"}}])
95
95
  self.assertIn("output[dim]:path[/dim]", result)
96
96
 
97
97
  def test_format_shows_datetime_type(self):
@@ -150,7 +150,7 @@ class TestFormatTaskArguments(unittest.TestCase):
150
150
  def test_format_escapes_rich_markup_in_defaults(self):
151
151
  """Test formatting properly escapes Rich markup in default values."""
152
152
  # Test with brackets in default value
153
- result = _format_task_arguments(["pattern=[a-z]+"])
153
+ result = _format_task_arguments([{"pattern": {"default": "[a-z]+"}}])
154
154
  # The brackets in the default should be escaped
155
155
  self.assertIn("[dim]\\[=[a-z]+][/dim]", result)
156
156
 
@@ -84,7 +84,7 @@ class TestParameterizedGraphConstruction(unittest.TestCase):
84
84
  "process": Task(
85
85
  name="process",
86
86
  cmd="echo mode={{arg.mode}}",
87
- args=["mode=debug"],
87
+ args=[{"mode": {"default": "debug"}}],
88
88
  deps=[],
89
89
  ),
90
90
  "consumer1": Task(
@@ -27,29 +27,23 @@ class TestParseArgSpec(unittest.TestCase):
27
27
  self.assertIsNone(spec.default)
28
28
  self.assertFalse(spec.is_exported)
29
29
 
30
- def test_parse_arg_with_default(self):
31
- """Test parsing argument with default value."""
32
- spec = parse_arg_spec("region=eu-west-1")
33
- self.assertEqual(spec.name,"region")
34
- self.assertEqual(spec.arg_type,"str")
35
- self.assertEqual(spec.default,"eu-west-1")
36
- self.assertFalse(spec.is_exported)
30
+ def test_parse_arg_with_default_raises_error(self):
31
+ """Test that string format with default raises error."""
32
+ with self.assertRaises(ValueError) as context:
33
+ parse_arg_spec("region=eu-west-1")
34
+ self.assertIn("Invalid argument syntax", str(context.exception))
37
35
 
38
- def test_parse_arg_with_type(self):
39
- """Test parsing argument with type."""
40
- spec = parse_arg_spec("port:int")
41
- self.assertEqual(spec.name,"port")
42
- self.assertEqual(spec.arg_type,"int")
43
- self.assertIsNone(spec.default)
44
- self.assertFalse(spec.is_exported)
36
+ def test_parse_arg_with_type_raises_error(self):
37
+ """Test that string format with type raises error."""
38
+ with self.assertRaises(ValueError) as context:
39
+ parse_arg_spec("port:int")
40
+ self.assertIn("Invalid argument syntax", str(context.exception))
45
41
 
46
- def test_parse_arg_with_type_and_default(self):
47
- """Test parsing argument with type and default."""
48
- spec = parse_arg_spec("port:int=8080")
49
- self.assertEqual(spec.name,"port")
50
- self.assertEqual(spec.arg_type,"int")
51
- self.assertEqual(spec.default,"8080")
52
- self.assertFalse(spec.is_exported)
42
+ def test_parse_arg_with_type_and_default_raises_error(self):
43
+ """Test that string format with type and default raises error."""
44
+ with self.assertRaises(ValueError) as context:
45
+ parse_arg_spec("port:int=8080")
46
+ self.assertIn("Invalid argument syntax", str(context.exception))
53
47
 
54
48
  def test_parse_exported_arg(self):
55
49
  """Test parsing exported argument ($ prefix)."""
@@ -59,39 +53,34 @@ class TestParseArgSpec(unittest.TestCase):
59
53
  self.assertIsNone(spec.default)
60
54
  self.assertTrue(spec.is_exported)
61
55
 
62
- def test_parse_exported_arg_with_default(self):
63
- """Test parsing exported argument with default value."""
64
- spec = parse_arg_spec("$user=admin")
65
- self.assertEqual(spec.name,"user")
66
- self.assertEqual(spec.arg_type,"str")
67
- self.assertEqual(spec.default,"admin")
68
- self.assertTrue(spec.is_exported)
56
+ def test_parse_exported_arg_with_default_raises_error(self):
57
+ """Test that exported argument string format with default raises error."""
58
+ with self.assertRaises(ValueError) as context:
59
+ parse_arg_spec("$user=admin")
60
+ self.assertIn("Invalid argument syntax", str(context.exception))
69
61
 
70
62
  def test_parse_exported_arg_with_type_raises_error(self):
71
63
  """Test that exported arguments with type annotations raise error."""
72
64
  with self.assertRaises(ValueError) as context:
73
65
  parse_arg_spec("$server:str")
74
- self.assertIn("Type annotations not allowed on exported arguments", str(context.exception))
75
- self.assertIn("$server:str", str(context.exception))
66
+ self.assertIn("Invalid argument syntax", str(context.exception))
76
67
 
77
68
  def test_parse_exported_arg_with_type_and_default_raises_error(self):
78
69
  """Test that exported arguments with type and default raise error."""
79
70
  with self.assertRaises(ValueError) as context:
80
71
  parse_arg_spec("$port:int=8080")
81
- self.assertIn("Type annotations not allowed on exported arguments", str(context.exception))
72
+ self.assertIn("Invalid argument syntax", str(context.exception))
82
73
 
83
74
  def test_yaml_parses_dollar_prefix_as_literal(self):
84
75
  """Test that PyYAML correctly parses $ prefix as literal text."""
85
76
  yaml_text = """
86
77
  args:
87
78
  - $server
88
- - $user=admin
89
- - port:int=8080
79
+ - environment
90
80
  """
91
81
  data = yaml.safe_load(yaml_text)
92
82
  self.assertEqual(data["args"][0], "$server")
93
- self.assertEqual(data["args"][1], "$user=admin")
94
- self.assertEqual(data["args"][2], "port:int=8080")
83
+ self.assertEqual(data["args"][1], "environment")
95
84
 
96
85
 
97
86
  class TestParseArgSpecYAML(unittest.TestCase):
@@ -416,7 +405,9 @@ tasks:
416
405
  inputs: ["src/**/*.rs"]
417
406
  outputs: [target/release/bin]
418
407
  working_dir: subproject
419
- args: [environment, region=eu-west-1]
408
+ args:
409
+ - environment
410
+ - region: { default: eu-west-1 }
420
411
  cmd: cargo build --release
421
412
  """
422
413
  )
@@ -428,7 +419,9 @@ tasks:
428
419
  self.assertEqual(task.inputs, ["src/**/*.rs"])
429
420
  self.assertEqual(task.outputs, ["target/release/bin"])
430
421
  self.assertEqual(task.working_dir, "subproject")
431
- self.assertEqual(task.args, ["environment", "region=eu-west-1"])
422
+ self.assertEqual(len(task.args), 2)
423
+ self.assertEqual(task.args[0], "environment")
424
+ self.assertIsInstance(task.args[1], dict)
432
425
  self.assertEqual(task.cmd, "cargo build --release")
433
426
 
434
427
  def test_parse_with_imports(self):
@@ -729,7 +722,9 @@ tasks:
729
722
  inputs: ["src/**/*.rs"]
730
723
  outputs: [target/release/bin]
731
724
  working_dir: subproject
732
- args: [environment, region=eu-west-1]
725
+ args:
726
+ - environment
727
+ - region: { default: eu-west-1 }
733
728
  cmd: cargo build --release
734
729
  """)
735
730
 
@@ -747,7 +742,7 @@ imports:
747
742
  self.assertEqual(task.inputs, ["src/**/*.rs"])
748
743
  self.assertEqual(task.outputs, ["target/release/bin"])
749
744
  self.assertEqual(task.working_dir, "subproject")
750
- self.assertEqual(task.args, ["environment", "region=eu-west-1"])
745
+ self.assertEqual(task.args, ["environment", {"region": {"default": "eu-west-1"}}])
751
746
  self.assertEqual(task.cmd, "cargo build --release")
752
747
 
753
748
  def test_cross_import_dependencies(self):
@@ -2664,7 +2659,7 @@ class TestArgMinMax(unittest.TestCase):
2664
2659
 
2665
2660
  def test_string_format_args_have_no_min_max(self):
2666
2661
  """Test that string format args return None for min/max."""
2667
- spec = parse_arg_spec("count:int=5")
2662
+ spec = parse_arg_spec("count")
2668
2663
  self.assertIsNone(spec.min_val)
2669
2664
  self.assertIsNone(spec.max_val)
2670
2665
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes