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.
Files changed (73) hide show
  1. {tasktree-0.0.11 → tasktree-0.0.12}/PKG-INFO +7 -4
  2. {tasktree-0.0.11 → tasktree-0.0.12}/README.md +6 -3
  3. {tasktree-0.0.11 → tasktree-0.0.12}/pyproject.toml +1 -1
  4. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/parser.py +41 -19
  5. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_variables.py +232 -0
  6. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_parser.py +3 -3
  7. {tasktree-0.0.11 → tasktree-0.0.12}/.claude/settings.local.json +0 -0
  8. {tasktree-0.0.11 → tasktree-0.0.12}/.github/workflows/claude-code-review.yml +0 -0
  9. {tasktree-0.0.11 → tasktree-0.0.12}/.github/workflows/claude.yml +0 -0
  10. {tasktree-0.0.11 → tasktree-0.0.12}/.github/workflows/release.yml +0 -0
  11. {tasktree-0.0.11 → tasktree-0.0.12}/.github/workflows/test.yml +0 -0
  12. {tasktree-0.0.11 → tasktree-0.0.12}/.gitignore +0 -0
  13. {tasktree-0.0.11 → tasktree-0.0.12}/.python-version +0 -0
  14. {tasktree-0.0.11 → tasktree-0.0.12}/CLAUDE.md +0 -0
  15. {tasktree-0.0.11 → tasktree-0.0.12}/example/source.txt +0 -0
  16. {tasktree-0.0.11 → tasktree-0.0.12}/example/tasktree.yaml +0 -0
  17. {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/01-basic-variables.md +0 -0
  18. {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/02-env-variable-type.md +0 -0
  19. {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/03-direct-env-substitution.md +0 -0
  20. {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/04-file-read-variables.md +0 -0
  21. {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/bug-report-dependency-triggering.md +0 -0
  22. {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/docker-task-environments.md +0 -0
  23. {tasktree-0.0.11 → tasktree-0.0.12}/requirements/implemented/shell-environment-requirements.md +0 -0
  24. {tasktree-0.0.11 → tasktree-0.0.12}/schema/README.md +0 -0
  25. {tasktree-0.0.11 → tasktree-0.0.12}/schema/tasktree-schema.json +0 -0
  26. {tasktree-0.0.11 → tasktree-0.0.12}/schema/vscode-settings-snippet.json +0 -0
  27. {tasktree-0.0.11 → tasktree-0.0.12}/src/__init__.py +0 -0
  28. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/__init__.py +0 -0
  29. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/cli.py +0 -0
  30. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/docker.py +0 -0
  31. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/executor.py +0 -0
  32. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/graph.py +0 -0
  33. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/hasher.py +0 -0
  34. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/state.py +0 -0
  35. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/substitution.py +0 -0
  36. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/tasks.py +0 -0
  37. {tasktree-0.0.11 → tasktree-0.0.12}/src/tasktree/types.py +0 -0
  38. {tasktree-0.0.11 → tasktree-0.0.12}/tasktree.yaml +0 -0
  39. {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/__init__.py +0 -0
  40. {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/test_docker_basic.py +0 -0
  41. {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/test_docker_environment.py +0 -0
  42. {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/test_docker_ownership.py +0 -0
  43. {tasktree-0.0.11 → tasktree-0.0.12}/tests/e2e/test_docker_volumes.py +0 -0
  44. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_arg_choices.py +0 -0
  45. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_arg_min_max.py +0 -0
  46. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_builtin_variables.py +0 -0
  47. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_clean_state.py +0 -0
  48. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_cli_options.py +0 -0
  49. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_dependency_execution.py +0 -0
  50. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_docker_build_args.py +0 -0
  51. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_end_to_end.py +0 -0
  52. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_exported_args.py +0 -0
  53. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_input_detection.py +0 -0
  54. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_missing_outputs.py +0 -0
  55. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_nested_imports.py +0 -0
  56. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_parameterized_dependencies.yaml +0 -0
  57. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_parameterized_deps_execution.py +0 -0
  58. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_state_persistence.py +0 -0
  59. {tasktree-0.0.11 → tasktree-0.0.12}/tests/integration/test_working_directory.py +0 -0
  60. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_cli.py +0 -0
  61. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_dependency_parsing.py +0 -0
  62. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_docker.py +0 -0
  63. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_environment_tracking.py +0 -0
  64. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_executor.py +0 -0
  65. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_graph.py +0 -0
  66. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_hasher.py +0 -0
  67. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_list_formatting.py +0 -0
  68. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_parameterized_graph.py +0 -0
  69. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_state.py +0 -0
  70. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_substitution.py +0 -0
  71. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_tasks.py +0 -0
  72. {tasktree-0.0.11 → tasktree-0.0.12}/tests/unit/test_types.py +0 -0
  73. {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.11
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
- # Direct env reference (resolved at parse time)
721
+ # Required env var (error if not set)
722
722
  api_key: { env: API_KEY }
723
- db_host: { env: DATABASE_HOST }
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
- # Direct env reference (resolved at parse time)
706
+ # Required env var (error if not set)
707
707
  api_key: { env: API_KEY }
708
- db_host: { env: DATABASE_HOST }
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tasktree"
3
- version = "0.0.11"
3
+ version = "0.0.12"
4
4
  description = "A task automation tool with incremental execution"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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
- Environment variable name
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
- if len(value) != 1:
305
- extra_keys = [k for k in value.keys() if k != "env"]
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 extra keys: {', '.join(extra_keys)}"
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 }}\n"
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
- return env_var_name
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, default: "foo" }
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("extra keys", error_msg.lower())
1718
- self.assertIn("default", error_msg)
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