tasktree 0.0.10__tar.gz → 0.0.12__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.
- {tasktree-0.0.10 → tasktree-0.0.12}/PKG-INFO +10 -5
- {tasktree-0.0.10 → tasktree-0.0.12}/README.md +9 -4
- {tasktree-0.0.10 → tasktree-0.0.12}/pyproject.toml +1 -1
- {tasktree-0.0.10 → tasktree-0.0.12}/schema/tasktree-schema.json +66 -1
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/cli.py +5 -3
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/parser.py +65 -46
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_end_to_end.py +5 -1
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_exported_args.py +24 -6
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_parameterized_dependencies.yaml +2 -2
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_parameterized_deps_execution.py +4 -4
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_variables.py +232 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_cli.py +4 -4
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_dependency_parsing.py +1 -1
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_list_formatting.py +9 -9
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_parameterized_graph.py +1 -1
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_parser.py +38 -43
- {tasktree-0.0.10 → tasktree-0.0.12}/.claude/settings.local.json +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/.github/workflows/claude-code-review.yml +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/.github/workflows/claude.yml +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/.github/workflows/release.yml +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/.github/workflows/test.yml +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/.gitignore +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/.python-version +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/CLAUDE.md +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/example/source.txt +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/example/tasktree.yaml +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/requirements/implemented/01-basic-variables.md +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/requirements/implemented/02-env-variable-type.md +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/requirements/implemented/03-direct-env-substitution.md +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/requirements/implemented/04-file-read-variables.md +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/requirements/implemented/bug-report-dependency-triggering.md +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/requirements/implemented/docker-task-environments.md +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/requirements/implemented/shell-environment-requirements.md +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/schema/README.md +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/schema/vscode-settings-snippet.json +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/__init__.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/__init__.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/docker.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/executor.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/graph.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/hasher.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/state.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/substitution.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/tasks.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/src/tasktree/types.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tasktree.yaml +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/e2e/__init__.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/e2e/test_docker_basic.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/e2e/test_docker_environment.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/e2e/test_docker_ownership.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/e2e/test_docker_volumes.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_arg_choices.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_arg_min_max.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_builtin_variables.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_clean_state.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_cli_options.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_dependency_execution.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_docker_build_args.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_input_detection.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_missing_outputs.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_nested_imports.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_state_persistence.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/integration/test_working_directory.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_docker.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_environment_tracking.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_executor.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_graph.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_hasher.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_state.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_substitution.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_tasks.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/tests/unit/test_types.py +0 -0
- {tasktree-0.0.10 → tasktree-0.0.12}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tasktree
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.12
|
|
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:
|
|
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)
|
|
@@ -716,10 +718,13 @@ For more complex scenarios, define environment variables in the `variables` sect
|
|
|
716
718
|
|
|
717
719
|
```yaml
|
|
718
720
|
variables:
|
|
719
|
-
#
|
|
721
|
+
# Required env var (error if not set)
|
|
720
722
|
api_key: { env: API_KEY }
|
|
721
|
-
|
|
722
|
-
|
|
723
|
+
|
|
724
|
+
# Optional env var with default
|
|
725
|
+
port: { env: PORT, default: "8080" }
|
|
726
|
+
log_level: { env: LOG_LEVEL, default: "info" }
|
|
727
|
+
|
|
723
728
|
# Or using string substitution
|
|
724
729
|
deploy_user: "{{ env.DEPLOY_USER }}"
|
|
725
730
|
|
|
@@ -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:
|
|
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)
|
|
@@ -701,10 +703,13 @@ For more complex scenarios, define environment variables in the `variables` sect
|
|
|
701
703
|
|
|
702
704
|
```yaml
|
|
703
705
|
variables:
|
|
704
|
-
#
|
|
706
|
+
# Required env var (error if not set)
|
|
705
707
|
api_key: { env: API_KEY }
|
|
706
|
-
|
|
707
|
-
|
|
708
|
+
|
|
709
|
+
# Optional env var with default
|
|
710
|
+
port: { env: PORT, default: "8080" }
|
|
711
|
+
log_level: { env: LOG_LEVEL, default: "info" }
|
|
712
|
+
|
|
708
713
|
# Or using string substitution
|
|
709
714
|
deploy_user: "{{ env.DEPLOY_USER }}"
|
|
710
715
|
|
|
@@ -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:
|
|
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
|
"""
|
|
@@ -287,26 +287,35 @@ def _is_env_variable_reference(value: Any) -> bool:
|
|
|
287
287
|
return isinstance(value, dict) and "env" in value
|
|
288
288
|
|
|
289
289
|
|
|
290
|
-
def _validate_env_variable_reference(var_name: str, value: dict) -> str:
|
|
291
|
-
"""Validate and extract environment variable name from reference.
|
|
290
|
+
def _validate_env_variable_reference(var_name: str, value: dict) -> tuple[str, str | None]:
|
|
291
|
+
"""Validate and extract environment variable name and optional default from reference.
|
|
292
292
|
|
|
293
293
|
Args:
|
|
294
294
|
var_name: Name of the variable being defined
|
|
295
|
-
value: Dict that should be { env: ENV_VAR_NAME }
|
|
295
|
+
value: Dict that should be { env: ENV_VAR_NAME } or { env: ENV_VAR_NAME, default: "value" }
|
|
296
296
|
|
|
297
297
|
Returns:
|
|
298
|
-
|
|
298
|
+
Tuple of (environment variable name, default value or None)
|
|
299
299
|
|
|
300
300
|
Raises:
|
|
301
301
|
ValueError: If reference is invalid
|
|
302
302
|
"""
|
|
303
|
-
# Validate dict structure
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
# Validate dict structure - allow 'env' and optionally 'default'
|
|
304
|
+
valid_keys = {"env", "default"}
|
|
305
|
+
invalid_keys = set(value.keys()) - valid_keys
|
|
306
|
+
if invalid_keys:
|
|
306
307
|
raise ValueError(
|
|
307
308
|
f"Invalid environment variable reference in variable '{var_name}'.\n"
|
|
308
|
-
f"Expected: {{ env: VARIABLE_NAME }}\n"
|
|
309
|
-
f"Found
|
|
309
|
+
f"Expected: {{ env: VARIABLE_NAME }} or {{ env: VARIABLE_NAME, default: \"value\" }}\n"
|
|
310
|
+
f"Found invalid keys: {', '.join(invalid_keys)}"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Validate 'env' key is present
|
|
314
|
+
if "env" not in value:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"Invalid environment variable reference in variable '{var_name}'.\n"
|
|
317
|
+
f"Missing required 'env' key.\n"
|
|
318
|
+
f"Expected: {{ env: VARIABLE_NAME }} or {{ env: VARIABLE_NAME, default: \"value\" }}"
|
|
310
319
|
)
|
|
311
320
|
|
|
312
321
|
env_var_name = value["env"]
|
|
@@ -315,7 +324,7 @@ def _validate_env_variable_reference(var_name: str, value: dict) -> str:
|
|
|
315
324
|
if not env_var_name or not isinstance(env_var_name, str):
|
|
316
325
|
raise ValueError(
|
|
317
326
|
f"Invalid environment variable reference in variable '{var_name}'.\n"
|
|
318
|
-
f"Expected: {{ env: VARIABLE_NAME }}\
|
|
327
|
+
f"Expected: {{ env: VARIABLE_NAME }} or {{ env: VARIABLE_NAME, default: \"value\" }}"
|
|
319
328
|
f"Found: {{ env: {env_var_name!r} }}"
|
|
320
329
|
)
|
|
321
330
|
|
|
@@ -327,23 +336,36 @@ def _validate_env_variable_reference(var_name: str, value: dict) -> str:
|
|
|
327
336
|
f"and contain only alphanumerics and underscores."
|
|
328
337
|
)
|
|
329
338
|
|
|
330
|
-
|
|
339
|
+
# Extract and validate default if present
|
|
340
|
+
default = value.get("default")
|
|
341
|
+
if default is not None:
|
|
342
|
+
# Default must be a string (env vars are always strings)
|
|
343
|
+
if not isinstance(default, str):
|
|
344
|
+
raise ValueError(
|
|
345
|
+
f"Invalid default value in variable '{var_name}'.\n"
|
|
346
|
+
f"Environment variable defaults must be strings.\n"
|
|
347
|
+
f"Got: {default!r} (type: {type(default).__name__})\n"
|
|
348
|
+
f"Use a quoted string: {{ env: {env_var_name}, default: \"{default}\" }}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return env_var_name, default
|
|
331
352
|
|
|
332
353
|
|
|
333
|
-
def _resolve_env_variable(var_name: str, env_var_name: str) -> str:
|
|
354
|
+
def _resolve_env_variable(var_name: str, env_var_name: str, default: str | None = None) -> str:
|
|
334
355
|
"""Resolve environment variable value.
|
|
335
356
|
|
|
336
357
|
Args:
|
|
337
358
|
var_name: Name of the variable being defined
|
|
338
359
|
env_var_name: Name of environment variable to read
|
|
360
|
+
default: Optional default value to use if environment variable is not set
|
|
339
361
|
|
|
340
362
|
Returns:
|
|
341
|
-
Environment variable value as string
|
|
363
|
+
Environment variable value as string, or default if not set and default provided
|
|
342
364
|
|
|
343
365
|
Raises:
|
|
344
|
-
ValueError: If environment variable is not set
|
|
366
|
+
ValueError: If environment variable is not set and no default provided
|
|
345
367
|
"""
|
|
346
|
-
value = os.environ.get(env_var_name)
|
|
368
|
+
value = os.environ.get(env_var_name, default)
|
|
347
369
|
|
|
348
370
|
if value is None:
|
|
349
371
|
raise ValueError(
|
|
@@ -730,11 +752,11 @@ def _resolve_variable_value(
|
|
|
730
752
|
|
|
731
753
|
# Check if this is an environment variable reference
|
|
732
754
|
if _is_env_variable_reference(raw_value):
|
|
733
|
-
# Validate and extract env var name
|
|
734
|
-
env_var_name = _validate_env_variable_reference(name, raw_value)
|
|
755
|
+
# Validate and extract env var name and optional default
|
|
756
|
+
env_var_name, default = _validate_env_variable_reference(name, raw_value)
|
|
735
757
|
|
|
736
|
-
# Resolve from os.environ
|
|
737
|
-
string_value = _resolve_env_variable(name, env_var_name)
|
|
758
|
+
# Resolve from os.environ (with optional default)
|
|
759
|
+
string_value = _resolve_env_variable(name, env_var_name, default)
|
|
738
760
|
|
|
739
761
|
# Still perform variable-in-variable substitution
|
|
740
762
|
from tasktree.substitution import substitute_variables
|
|
@@ -1259,18 +1281,16 @@ def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
|
|
|
1259
1281
|
|
|
1260
1282
|
Supports both string format and dictionary format:
|
|
1261
1283
|
|
|
1262
|
-
String format:
|
|
1284
|
+
String format (simple names only):
|
|
1263
1285
|
- Simple name: "argname"
|
|
1264
1286
|
- Exported (becomes env var): "$argname"
|
|
1265
|
-
- With default: "argname=value" or "$argname=value"
|
|
1266
|
-
- Legacy type syntax: "argname:type=value" (for backwards compat)
|
|
1267
1287
|
|
|
1268
1288
|
Dictionary format:
|
|
1269
1289
|
- argname: { default: "value" }
|
|
1270
1290
|
- argname: { type: int, default: 42 }
|
|
1271
1291
|
- argname: { type: int, min: 1, max: 100 }
|
|
1272
1292
|
- argname: { type: str, choices: ["dev", "staging", "prod"] }
|
|
1273
|
-
- $argname: { default: "value" } # Exported
|
|
1293
|
+
- $argname: { default: "value" } # Exported (type not allowed)
|
|
1274
1294
|
|
|
1275
1295
|
Args:
|
|
1276
1296
|
arg_spec: Argument specification (string or dict with single key)
|
|
@@ -1328,34 +1348,24 @@ def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
|
|
|
1328
1348
|
if is_exported:
|
|
1329
1349
|
arg_spec = arg_spec[1:] # Remove $ prefix
|
|
1330
1350
|
|
|
1331
|
-
#
|
|
1332
|
-
if "=" in arg_spec:
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1351
|
+
# String format only supports simple names (no = or :)
|
|
1352
|
+
if "=" in arg_spec or ":" in arg_spec:
|
|
1353
|
+
raise ValueError(
|
|
1354
|
+
f"Invalid argument syntax: {'$' if is_exported else ''}{arg_spec}\n\n"
|
|
1355
|
+
f"String format only supports simple argument names.\n"
|
|
1356
|
+
f"Use YAML dict format for type annotations, defaults, or constraints:\n"
|
|
1357
|
+
f" args:\n"
|
|
1358
|
+
f" - {'$' if is_exported else ''}{arg_spec.split('=')[0].split(':')[0]}: {{ default: value }}"
|
|
1359
|
+
)
|
|
1337
1360
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
name, arg_type = name_type.split(":", 1)
|
|
1361
|
+
name = arg_spec
|
|
1362
|
+
arg_type = "str"
|
|
1341
1363
|
|
|
1342
|
-
|
|
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
|
|
1364
|
+
# String format doesn't support min/max/choices/defaults
|
|
1355
1365
|
return ArgSpec(
|
|
1356
1366
|
name=name,
|
|
1357
1367
|
arg_type=arg_type,
|
|
1358
|
-
default=
|
|
1368
|
+
default=None,
|
|
1359
1369
|
is_exported=is_exported,
|
|
1360
1370
|
min_val=None,
|
|
1361
1371
|
max_val=None,
|
|
@@ -1403,6 +1413,15 @@ def _parse_arg_dict(arg_name: str, config: dict, is_exported: bool) -> ArgSpec:
|
|
|
1403
1413
|
f"Exported arguments are always strings. Remove the 'type' field"
|
|
1404
1414
|
)
|
|
1405
1415
|
|
|
1416
|
+
# Exported arguments must have string defaults (if any default is provided)
|
|
1417
|
+
if is_exported and default is not None and not isinstance(default, str):
|
|
1418
|
+
raise ValueError(
|
|
1419
|
+
f"Exported argument '${arg_name}' must have a string default value.\n"
|
|
1420
|
+
f"Got: {default!r} (type: {type(default).__name__})\n"
|
|
1421
|
+
f"Exported arguments become environment variables, which are always strings.\n"
|
|
1422
|
+
f"Use a quoted string: ${arg_name}: {{ default: \"{default}\" }}"
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1406
1425
|
# Validate choices
|
|
1407
1426
|
if choices is not None:
|
|
1408
1427
|
# 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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."""
|
|
@@ -37,8 +37,8 @@ class TestParameterizedDependencyExecution(unittest.TestCase):
|
|
|
37
37
|
tasks:
|
|
38
38
|
build:
|
|
39
39
|
args:
|
|
40
|
-
- mode
|
|
41
|
-
- optimize:bool
|
|
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
|
|
155
|
-
- pretty:bool
|
|
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}}
|
|
@@ -693,6 +693,238 @@ tasks:
|
|
|
693
693
|
finally:
|
|
694
694
|
os.chdir(original_cwd)
|
|
695
695
|
|
|
696
|
+
def test_env_variable_with_default_when_not_set(self):
|
|
697
|
+
"""Test default value is used when environment variable is not set."""
|
|
698
|
+
# Ensure env var is NOT set
|
|
699
|
+
if "TEST_PORT_DEFAULT" in os.environ:
|
|
700
|
+
del os.environ["TEST_PORT_DEFAULT"]
|
|
701
|
+
|
|
702
|
+
with TemporaryDirectory() as tmpdir:
|
|
703
|
+
project_root = Path(tmpdir)
|
|
704
|
+
|
|
705
|
+
recipe_file = project_root / "tasktree.yaml"
|
|
706
|
+
recipe_file.write_text("""
|
|
707
|
+
variables:
|
|
708
|
+
port: { env: TEST_PORT_DEFAULT, default: "8080" }
|
|
709
|
+
|
|
710
|
+
tasks:
|
|
711
|
+
test:
|
|
712
|
+
outputs: [config.txt]
|
|
713
|
+
cmd: echo "Port={{ var.port }}" > config.txt
|
|
714
|
+
""")
|
|
715
|
+
|
|
716
|
+
original_cwd = os.getcwd()
|
|
717
|
+
try:
|
|
718
|
+
os.chdir(project_root)
|
|
719
|
+
|
|
720
|
+
# Run task
|
|
721
|
+
result = self.runner.invoke(app, ["test"], env=self.env)
|
|
722
|
+
self.assertEqual(result.exit_code, 0)
|
|
723
|
+
|
|
724
|
+
# Verify default value was used
|
|
725
|
+
output_file = project_root / "config.txt"
|
|
726
|
+
self.assertTrue(output_file.exists())
|
|
727
|
+
content = output_file.read_text().strip()
|
|
728
|
+
self.assertEqual(content, "Port=8080")
|
|
729
|
+
|
|
730
|
+
finally:
|
|
731
|
+
os.chdir(original_cwd)
|
|
732
|
+
|
|
733
|
+
def test_env_variable_with_default_when_set(self):
|
|
734
|
+
"""Test environment variable value is used when set, ignoring default."""
|
|
735
|
+
# Set environment variable
|
|
736
|
+
os.environ["TEST_PORT_OVERRIDE"] = "9000"
|
|
737
|
+
|
|
738
|
+
try:
|
|
739
|
+
with TemporaryDirectory() as tmpdir:
|
|
740
|
+
project_root = Path(tmpdir)
|
|
741
|
+
|
|
742
|
+
recipe_file = project_root / "tasktree.yaml"
|
|
743
|
+
recipe_file.write_text("""
|
|
744
|
+
variables:
|
|
745
|
+
port: { env: TEST_PORT_OVERRIDE, default: "8080" }
|
|
746
|
+
|
|
747
|
+
tasks:
|
|
748
|
+
test:
|
|
749
|
+
outputs: [config.txt]
|
|
750
|
+
cmd: echo "Port={{ var.port }}" > config.txt
|
|
751
|
+
""")
|
|
752
|
+
|
|
753
|
+
original_cwd = os.getcwd()
|
|
754
|
+
try:
|
|
755
|
+
os.chdir(project_root)
|
|
756
|
+
|
|
757
|
+
# Run task
|
|
758
|
+
result = self.runner.invoke(app, ["test"], env=self.env)
|
|
759
|
+
self.assertEqual(result.exit_code, 0)
|
|
760
|
+
|
|
761
|
+
# Verify env var value was used, not default
|
|
762
|
+
output_file = project_root / "config.txt"
|
|
763
|
+
self.assertTrue(output_file.exists())
|
|
764
|
+
content = output_file.read_text().strip()
|
|
765
|
+
self.assertEqual(content, "Port=9000")
|
|
766
|
+
|
|
767
|
+
finally:
|
|
768
|
+
os.chdir(original_cwd)
|
|
769
|
+
|
|
770
|
+
finally:
|
|
771
|
+
del os.environ["TEST_PORT_OVERRIDE"]
|
|
772
|
+
|
|
773
|
+
def test_env_variable_empty_string_vs_default(self):
|
|
774
|
+
"""Test that empty string env var is used, not the default."""
|
|
775
|
+
# Set environment variable to empty string
|
|
776
|
+
os.environ["TEST_EMPTY_VAR"] = ""
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
with TemporaryDirectory() as tmpdir:
|
|
780
|
+
project_root = Path(tmpdir)
|
|
781
|
+
|
|
782
|
+
recipe_file = project_root / "tasktree.yaml"
|
|
783
|
+
recipe_file.write_text("""
|
|
784
|
+
variables:
|
|
785
|
+
value: { env: TEST_EMPTY_VAR, default: "default_value" }
|
|
786
|
+
|
|
787
|
+
tasks:
|
|
788
|
+
test:
|
|
789
|
+
outputs: [output.txt]
|
|
790
|
+
cmd: echo "Value=[{{ var.value }}]" > output.txt
|
|
791
|
+
""")
|
|
792
|
+
|
|
793
|
+
original_cwd = os.getcwd()
|
|
794
|
+
try:
|
|
795
|
+
os.chdir(project_root)
|
|
796
|
+
|
|
797
|
+
# Run task
|
|
798
|
+
result = self.runner.invoke(app, ["test"], env=self.env)
|
|
799
|
+
self.assertEqual(result.exit_code, 0)
|
|
800
|
+
|
|
801
|
+
# Verify empty string was used (not default)
|
|
802
|
+
output_file = project_root / "output.txt"
|
|
803
|
+
self.assertTrue(output_file.exists())
|
|
804
|
+
content = output_file.read_text().strip()
|
|
805
|
+
self.assertEqual(content, "Value=[]")
|
|
806
|
+
|
|
807
|
+
finally:
|
|
808
|
+
os.chdir(original_cwd)
|
|
809
|
+
|
|
810
|
+
finally:
|
|
811
|
+
del os.environ["TEST_EMPTY_VAR"]
|
|
812
|
+
|
|
813
|
+
def test_env_variable_default_must_be_string(self):
|
|
814
|
+
"""Test that non-string defaults are rejected."""
|
|
815
|
+
with TemporaryDirectory() as tmpdir:
|
|
816
|
+
project_root = Path(tmpdir)
|
|
817
|
+
|
|
818
|
+
recipe_file = project_root / "tasktree.yaml"
|
|
819
|
+
recipe_file.write_text("""
|
|
820
|
+
variables:
|
|
821
|
+
port: { env: TEST_PORT, default: 8080 }
|
|
822
|
+
|
|
823
|
+
tasks:
|
|
824
|
+
test:
|
|
825
|
+
cmd: echo test
|
|
826
|
+
""")
|
|
827
|
+
|
|
828
|
+
original_cwd = os.getcwd()
|
|
829
|
+
try:
|
|
830
|
+
os.chdir(project_root)
|
|
831
|
+
|
|
832
|
+
# Should fail at parse time with clear error
|
|
833
|
+
result = self.runner.invoke(app, ["test"], env=self.env)
|
|
834
|
+
self.assertNotEqual(result.exit_code, 0)
|
|
835
|
+
|
|
836
|
+
# Error should mention that default must be string
|
|
837
|
+
output = result.stdout
|
|
838
|
+
self.assertIn("default", output.lower())
|
|
839
|
+
self.assertIn("string", output.lower())
|
|
840
|
+
|
|
841
|
+
finally:
|
|
842
|
+
os.chdir(original_cwd)
|
|
843
|
+
|
|
844
|
+
def test_env_variable_multiple_with_defaults(self):
|
|
845
|
+
"""Test multiple env variables with defaults work together."""
|
|
846
|
+
# Set only one env var
|
|
847
|
+
os.environ["TEST_HOST"] = "prod.example.com"
|
|
848
|
+
|
|
849
|
+
try:
|
|
850
|
+
# Ensure other env var is NOT set
|
|
851
|
+
if "TEST_PORT" in os.environ:
|
|
852
|
+
del os.environ["TEST_PORT"]
|
|
853
|
+
|
|
854
|
+
with TemporaryDirectory() as tmpdir:
|
|
855
|
+
project_root = Path(tmpdir)
|
|
856
|
+
|
|
857
|
+
recipe_file = project_root / "tasktree.yaml"
|
|
858
|
+
recipe_file.write_text("""
|
|
859
|
+
variables:
|
|
860
|
+
host: { env: TEST_HOST, default: "localhost" }
|
|
861
|
+
port: { env: TEST_PORT, default: "8080" }
|
|
862
|
+
url: "{{ var.host }}:{{ var.port }}"
|
|
863
|
+
|
|
864
|
+
tasks:
|
|
865
|
+
test:
|
|
866
|
+
outputs: [config.txt]
|
|
867
|
+
cmd: echo "URL={{ var.url }}" > config.txt
|
|
868
|
+
""")
|
|
869
|
+
|
|
870
|
+
original_cwd = os.getcwd()
|
|
871
|
+
try:
|
|
872
|
+
os.chdir(project_root)
|
|
873
|
+
|
|
874
|
+
# Run task
|
|
875
|
+
result = self.runner.invoke(app, ["test"], env=self.env)
|
|
876
|
+
self.assertEqual(result.exit_code, 0)
|
|
877
|
+
|
|
878
|
+
# Verify one used env var, one used default
|
|
879
|
+
output_file = project_root / "config.txt"
|
|
880
|
+
self.assertTrue(output_file.exists())
|
|
881
|
+
content = output_file.read_text().strip()
|
|
882
|
+
self.assertEqual(content, "URL=prod.example.com:8080")
|
|
883
|
+
|
|
884
|
+
finally:
|
|
885
|
+
os.chdir(original_cwd)
|
|
886
|
+
|
|
887
|
+
finally:
|
|
888
|
+
del os.environ["TEST_HOST"]
|
|
889
|
+
|
|
890
|
+
def test_env_variable_default_with_variable_substitution(self):
|
|
891
|
+
"""Test default value can contain variable references."""
|
|
892
|
+
# Ensure env var is NOT set
|
|
893
|
+
if "TEST_OVERRIDE" in os.environ:
|
|
894
|
+
del os.environ["TEST_OVERRIDE"]
|
|
895
|
+
|
|
896
|
+
with TemporaryDirectory() as tmpdir:
|
|
897
|
+
project_root = Path(tmpdir)
|
|
898
|
+
|
|
899
|
+
recipe_file = project_root / "tasktree.yaml"
|
|
900
|
+
recipe_file.write_text("""
|
|
901
|
+
variables:
|
|
902
|
+
base_url: "https://api.example.com"
|
|
903
|
+
endpoint: { env: TEST_OVERRIDE, default: "{{ var.base_url }}/users" }
|
|
904
|
+
|
|
905
|
+
tasks:
|
|
906
|
+
test:
|
|
907
|
+
outputs: [config.txt]
|
|
908
|
+
cmd: echo "Endpoint={{ var.endpoint }}" > config.txt
|
|
909
|
+
""")
|
|
910
|
+
|
|
911
|
+
original_cwd = os.getcwd()
|
|
912
|
+
try:
|
|
913
|
+
os.chdir(project_root)
|
|
914
|
+
|
|
915
|
+
# Run task
|
|
916
|
+
result = self.runner.invoke(app, ["test"], env=self.env)
|
|
917
|
+
self.assertEqual(result.exit_code, 0)
|
|
918
|
+
|
|
919
|
+
# Verify default with variable substitution worked
|
|
920
|
+
output_file = project_root / "config.txt"
|
|
921
|
+
self.assertTrue(output_file.exists())
|
|
922
|
+
content = output_file.read_text().strip()
|
|
923
|
+
self.assertEqual(content, "Endpoint=https://api.example.com/users")
|
|
924
|
+
|
|
925
|
+
finally:
|
|
926
|
+
os.chdir(original_cwd)
|
|
927
|
+
|
|
696
928
|
|
|
697
929
|
if __name__ == "__main__":
|
|
698
930
|
unittest.main()
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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
|
|
31
|
-
"""Test
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
self.
|
|
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
|
|
39
|
-
"""Test
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
self.
|
|
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
|
|
47
|
-
"""Test
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
self.
|
|
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
|
|
63
|
-
"""Test
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
self.
|
|
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("
|
|
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("
|
|
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
|
-
-
|
|
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], "
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
|
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):
|
|
@@ -1708,7 +1703,7 @@ tasks:
|
|
|
1708
1703
|
recipe_path = Path(tmpdir) / "tasktree.yaml"
|
|
1709
1704
|
recipe_path.write_text("""
|
|
1710
1705
|
variables:
|
|
1711
|
-
my_var: { env: TEST_VAR,
|
|
1706
|
+
my_var: { env: TEST_VAR, foo: "bar" }
|
|
1712
1707
|
|
|
1713
1708
|
tasks:
|
|
1714
1709
|
test:
|
|
@@ -1719,8 +1714,8 @@ tasks:
|
|
|
1719
1714
|
parse_recipe(recipe_path)
|
|
1720
1715
|
|
|
1721
1716
|
error_msg = str(cm.exception)
|
|
1722
|
-
self.assertIn("
|
|
1723
|
-
self.assertIn("
|
|
1717
|
+
self.assertIn("found invalid keys", error_msg.lower())
|
|
1718
|
+
self.assertIn("foo", error_msg)
|
|
1724
1719
|
|
|
1725
1720
|
def test_parse_env_variable_invalid_name_empty(self):
|
|
1726
1721
|
"""Test error for { env: } with empty value."""
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tasktree-0.0.10 → tasktree-0.0.12}/requirements/implemented/bug-report-dependency-triggering.md
RENAMED
|
File without changes
|
|
File without changes
|
{tasktree-0.0.10 → tasktree-0.0.12}/requirements/implemented/shell-environment-requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|