tasktree 0.0.11__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.11 → tasktree-0.0.12}/PKG-INFO +7 -4
- {tasktree-0.0.11 → tasktree-0.0.12}/README.md +6 -3
- {tasktree-0.0.11 → tasktree-0.0.12}/pyproject.toml +1 -1
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/parser.py +41 -19
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_variables.py +232 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_parser.py +3 -3
- {tasktree-0.0.11 → tasktree-0.0.12}/.claude/settings.local.json +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/.github/workflows/claude-code-review.yml +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/.github/workflows/claude.yml +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/.github/workflows/release.yml +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/.github/workflows/test.yml +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/.gitignore +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/.python-version +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/CLAUDE.md +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/example/source.txt +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/example/tasktree.yaml +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/01-basic-variables.md +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/02-env-variable-type.md +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/03-direct-env-substitution.md +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/04-file-read-variables.md +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/bug-report-dependency-triggering.md +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/docker-task-environments.md +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/shell-environment-requirements.md +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/schema/README.md +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/schema/tasktree-schema.json +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/schema/vscode-settings-snippet.json +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/__init__.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/__init__.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/cli.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/docker.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/executor.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/graph.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/hasher.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/state.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/substitution.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/tasks.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/types.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tasktree.yaml +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/__init__.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/test_docker_basic.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/test_docker_environment.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/test_docker_ownership.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/test_docker_volumes.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_arg_choices.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_arg_min_max.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_builtin_variables.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_clean_state.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_cli_options.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_dependency_execution.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_docker_build_args.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_end_to_end.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_exported_args.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_input_detection.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_missing_outputs.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_nested_imports.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_parameterized_dependencies.yaml +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_parameterized_deps_execution.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_state_persistence.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_working_directory.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_cli.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_dependency_parsing.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_docker.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_environment_tracking.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_executor.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_graph.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_hasher.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_list_formatting.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_parameterized_graph.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_state.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_substitution.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_tasks.py +0 -0
- {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_types.py +0 -0
- {tasktree-0.0.11 → 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
|
|
@@ -718,10 +718,13 @@ For more complex scenarios, define environment variables in the `variables` sect
|
|
|
718
718
|
|
|
719
719
|
```yaml
|
|
720
720
|
variables:
|
|
721
|
-
#
|
|
721
|
+
# Required env var (error if not set)
|
|
722
722
|
api_key: { env: API_KEY }
|
|
723
|
-
|
|
724
|
-
|
|
723
|
+
|
|
724
|
+
# Optional env var with default
|
|
725
|
+
port: { env: PORT, default: "8080" }
|
|
726
|
+
log_level: { env: LOG_LEVEL, default: "info" }
|
|
727
|
+
|
|
725
728
|
# Or using string substitution
|
|
726
729
|
deploy_user: "{{ env.DEPLOY_USER }}"
|
|
727
730
|
|
|
@@ -703,10 +703,13 @@ For more complex scenarios, define environment variables in the `variables` sect
|
|
|
703
703
|
|
|
704
704
|
```yaml
|
|
705
705
|
variables:
|
|
706
|
-
#
|
|
706
|
+
# Required env var (error if not set)
|
|
707
707
|
api_key: { env: API_KEY }
|
|
708
|
-
|
|
709
|
-
|
|
708
|
+
|
|
709
|
+
# Optional env var with default
|
|
710
|
+
port: { env: PORT, default: "8080" }
|
|
711
|
+
log_level: { env: LOG_LEVEL, default: "info" }
|
|
712
|
+
|
|
710
713
|
# Or using string substitution
|
|
711
714
|
deploy_user: "{{ env.DEPLOY_USER }}"
|
|
712
715
|
|
|
@@ -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
|
|
@@ -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()
|
|
@@ -1703,7 +1703,7 @@ tasks:
|
|
|
1703
1703
|
recipe_path = Path(tmpdir) / "tasktree.yaml"
|
|
1704
1704
|
recipe_path.write_text("""
|
|
1705
1705
|
variables:
|
|
1706
|
-
my_var: { env: TEST_VAR,
|
|
1706
|
+
my_var: { env: TEST_VAR, foo: "bar" }
|
|
1707
1707
|
|
|
1708
1708
|
tasks:
|
|
1709
1709
|
test:
|
|
@@ -1714,8 +1714,8 @@ tasks:
|
|
|
1714
1714
|
parse_recipe(recipe_path)
|
|
1715
1715
|
|
|
1716
1716
|
error_msg = str(cm.exception)
|
|
1717
|
-
self.assertIn("
|
|
1718
|
-
self.assertIn("
|
|
1717
|
+
self.assertIn("found invalid keys", error_msg.lower())
|
|
1718
|
+
self.assertIn("foo", error_msg)
|
|
1719
1719
|
|
|
1720
1720
|
def test_parse_env_variable_invalid_name_empty(self):
|
|
1721
1721
|
"""Test error for { env: } with empty value."""
|
|
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.11 → tasktree-0.0.12}/requirements/implemented/bug-report-dependency-triggering.md
RENAMED
|
File without changes
|
|
File without changes
|
{tasktree-0.0.11 → 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
|
|
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
|