envstack 0.8.0__tar.gz → 0.8.2__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 (29) hide show
  1. {envstack-0.8.0/lib/envstack.egg-info → envstack-0.8.2}/PKG-INFO +1 -1
  2. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/__init__.py +1 -1
  3. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/util.py +18 -11
  4. {envstack-0.8.0 → envstack-0.8.2/lib/envstack.egg-info}/PKG-INFO +1 -1
  5. {envstack-0.8.0 → envstack-0.8.2}/setup.py +1 -1
  6. {envstack-0.8.0 → envstack-0.8.2}/tests/test_cmds.py +221 -1
  7. {envstack-0.8.0 → envstack-0.8.2}/tests/test_env.py +183 -11
  8. {envstack-0.8.0 → envstack-0.8.2}/tests/test_util.py +193 -0
  9. {envstack-0.8.0 → envstack-0.8.2}/LICENSE +0 -0
  10. {envstack-0.8.0 → envstack-0.8.2}/README.md +0 -0
  11. {envstack-0.8.0 → envstack-0.8.2}/dist.json +0 -0
  12. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/cli.py +0 -0
  13. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/config.py +0 -0
  14. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/encrypt.py +0 -0
  15. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/env.py +0 -0
  16. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/exceptions.py +0 -0
  17. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/logger.py +0 -0
  18. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/node.py +0 -0
  19. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/path.py +0 -0
  20. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack/wrapper.py +0 -0
  21. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack.egg-info/SOURCES.txt +0 -0
  22. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack.egg-info/dependency_links.txt +0 -0
  23. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack.egg-info/entry_points.txt +0 -0
  24. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack.egg-info/not-zip-safe +0 -0
  25. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack.egg-info/requires.txt +0 -0
  26. {envstack-0.8.0 → envstack-0.8.2}/lib/envstack.egg-info/top_level.txt +0 -0
  27. {envstack-0.8.0 → envstack-0.8.2}/setup.cfg +0 -0
  28. {envstack-0.8.0 → envstack-0.8.2}/tests/test_encrypt.py +0 -0
  29. {envstack-0.8.0 → envstack-0.8.2}/tests/test_node.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: envstack
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Stacked environment variable management system
5
5
  Home-page: http://github.com/rsgalloway/envstack
6
6
  Author: Ryan Galloway
@@ -34,6 +34,6 @@ Stacked environment variable management system.
34
34
  """
35
35
 
36
36
  __prog__ = "envstack"
37
- __version__ = "0.8.0"
37
+ __version__ = "0.8.2"
38
38
 
39
39
  from envstack.env import clear, init, revert, save # noqa: F401
@@ -51,7 +51,7 @@ null = ""
51
51
 
52
52
  # regular expression pattern for bash-like variable expansion
53
53
  variable_pattern = re.compile(
54
- r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([=?])(\$\{[a-zA-Z_][a-zA-Z0-9_]*\}|[^}]*))?\}"
54
+ r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([=?])(\$\{[^}]*\}[\-\w/]*|[^}]*))?\}"
55
55
  )
56
56
 
57
57
  # regular expression pattern for matching windows drive letters
@@ -311,8 +311,17 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
311
311
  # substitute all matches in the expression
312
312
  result = variable_pattern.sub(substitute_variable, expression)
313
313
 
314
+ # HACK: remove trailing curly braces if they exist
315
+ if result.endswith("}") and not result.startswith("${"):
316
+ result = result.rstrip("}")
317
+
318
+ # evaluate any remaining modifiers, eg. ${VAR:=${FOO:=bar}}
319
+ if variable_pattern.search(result):
320
+ result = evaluate_modifiers(result, environ)
321
+
314
322
  # dedupe path-like values and resolve separators
315
- if ":" in result and ("/" in result or "\\" in result):
323
+ # TODO: replace with regex pattern to detect path-like strings
324
+ elif ":" in result and ("/" in result or "\\" in result):
316
325
  result = dedupe_paths(result)
317
326
 
318
327
  # detect recursion errors
@@ -413,7 +422,7 @@ def get_stacks():
413
422
  return sorted(list(stacks))
414
423
 
415
424
 
416
- def findenv(var_name):
425
+ def findenv(var_name: str):
417
426
  """
418
427
  Returns a list of paths where the given environment var is set.
419
428
 
@@ -552,7 +561,7 @@ def dump_yaml(file_path: str, data: dict, unquote: bool = True):
552
561
  pass
553
562
 
554
563
 
555
- def partition_platform_data(data):
564
+ def partition_platform_data(data: dict):
556
565
  """
557
566
  Given a data dictionary with keys 'all', 'darwin', 'linux', 'windows',
558
567
  this function finds which key-value pairs are common across all platforms,
@@ -562,13 +571,11 @@ def partition_platform_data(data):
562
571
  :param data: dictionary to partition.
563
572
  :returns: platform partitioned dictionary.
564
573
  """
565
-
566
- # move data under the "all" key if it's not already there
574
+ # ensure all is present
567
575
  if "all" not in data:
568
- data["all"] = data.copy()
576
+ data["all"] = {} # data.copy()
569
577
 
570
578
  # platforms of interest (darwin, linux, windows)
571
- # platforms = [k for k in data.keys() if k not in ("all", "include")]
572
579
  platforms = ["darwin", "linux", "windows"]
573
580
 
574
581
  # get the union of keys from all platforms
@@ -581,10 +588,8 @@ def partition_platform_data(data):
581
588
  for key in all_platform_keys:
582
589
  if all(key in data[p] for p in platforms):
583
590
  # get first value for comparison later
584
- # first_value = data[platforms[0]][key].value
585
591
  first_value = data[platforms[0]][key]
586
592
  # call it common if all platforms have the same value
587
- # if all(data[p][key].value == first_value for p in platforms):
588
593
  if all(data[p][key] == first_value for p in platforms):
589
594
  common_keys.append(key)
590
595
 
@@ -614,8 +619,10 @@ def partition_platform_data(data):
614
619
  for p in platforms:
615
620
  new_data[p] = new_platform_dicts[p]
616
621
 
617
- # add include if it exists
622
+ # ensure include is present
618
623
  if data.get("include"):
619
624
  new_data["include"] = data["include"]
625
+ else:
626
+ new_data["include"] = []
620
627
 
621
628
  return new_data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: envstack
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Stacked environment variable management system
5
5
  Home-page: http://github.com/rsgalloway/envstack
6
6
  Author: Ryan Galloway
@@ -40,7 +40,7 @@ with open(os.path.join(here, "README.md")) as f:
40
40
 
41
41
  setup(
42
42
  name="envstack",
43
- version="0.8.0",
43
+ version="0.8.2",
44
44
  description="Stacked environment variable management system",
45
45
  long_description=long_description,
46
46
  long_description_content_type="text/markdown",
@@ -350,6 +350,222 @@ HELLO=goodbye
350
350
  self.assertEqual(output, expected_output)
351
351
 
352
352
 
353
+ class TestBake(unittest.TestCase):
354
+ """Tests bake command."""
355
+
356
+ def setUp(self):
357
+ self.filename = "baketest.env"
358
+ if os.path.exists(self.filename):
359
+ os.remove(self.filename)
360
+ self.envstack_bin = os.path.join(
361
+ os.path.dirname(__file__), "..", "bin", "envstack"
362
+ )
363
+ envpath = os.path.join(os.path.dirname(__file__), "..", "env")
364
+ self.root = {
365
+ "linux": "/mnt/pipe",
366
+ "win32": "X:/pipe",
367
+ "darwin": "/Volumes/pipe",
368
+ }.get(sys.platform)
369
+ os.environ["ENVPATH"] = envpath
370
+ os.environ["INTERACTIVE"] = "0"
371
+ # remove so we use base64 encoding by default
372
+ if AESGCMEncryptor.KEY_VAR_NAME in os.environ:
373
+ del os.environ[AESGCMEncryptor.KEY_VAR_NAME]
374
+ if FernetEncryptor.KEY_VAR_NAME in os.environ:
375
+ del os.environ[FernetEncryptor.KEY_VAR_NAME]
376
+
377
+ def tearDown(self):
378
+ if os.path.exists(self.filename):
379
+ os.remove(self.filename)
380
+
381
+ def test_default(self):
382
+ """Tests baking the default stack."""
383
+ command = f"%s -o {self.filename}; cat {self.filename}" % self.envstack_bin
384
+ expected_output = """#!/usr/bin/env envstack
385
+ include: []
386
+ all: &all
387
+ <<: *all
388
+ DEPLOY_ROOT: ${ROOT}/${ENV}
389
+ ENV: prod
390
+ ENVPATH: ${DEPLOY_ROOT}/env:${ENVPATH}
391
+ HELLO: ${HELLO:=world}
392
+ LOG_LEVEL: ${LOG_LEVEL:=INFO}
393
+ PATH: ${DEPLOY_ROOT}/bin:${PATH}
394
+ PYTHONPATH: ${DEPLOY_ROOT}/lib/python:${PYTHONPATH}
395
+ darwin:
396
+ <<: *all
397
+ ROOT: /Volumes/pipe
398
+ linux:
399
+ <<: *all
400
+ ROOT: /mnt/pipe
401
+ windows:
402
+ <<: *all
403
+ ROOT: X:/pipe
404
+ """
405
+ output = subprocess.check_output(
406
+ command,
407
+ shell=True,
408
+ universal_newlines=True,
409
+ )
410
+ self.assertEqual(output, expected_output)
411
+
412
+ def test_dev(self):
413
+ """Tests baking the dev stack."""
414
+ command = f"%s dev -o {self.filename}; cat {self.filename}" % self.envstack_bin
415
+ expected_output = """#!/usr/bin/env envstack
416
+ include: []
417
+ all: &all
418
+ <<: *all
419
+ DEPLOY_ROOT: ${ROOT}/dev
420
+ ENV: dev
421
+ ENVPATH: ${ROOT}/dev/env:${ROOT}/prod/env:${ENVPATH}
422
+ HELLO: ${HELLO:=world}
423
+ LOG_LEVEL: DEBUG
424
+ PATH: ${ROOT}/dev/bin:${ROOT}/prod/bin:${PATH}
425
+ PYTHONPATH: ${ROOT}/dev/lib/python:${ROOT}/prod/lib/python:${PYTHONPATH}
426
+ darwin:
427
+ <<: *all
428
+ ROOT: /Volumes/pipe
429
+ linux:
430
+ <<: *all
431
+ ROOT: /mnt/pipe
432
+ windows:
433
+ <<: *all
434
+ ROOT: X:/pipe
435
+ """
436
+ output = subprocess.check_output(
437
+ command,
438
+ shell=True,
439
+ universal_newlines=True,
440
+ )
441
+ self.assertEqual(output, expected_output)
442
+
443
+ def test_thing(self):
444
+ """Tests baking the thing stack with depth of 1."""
445
+ command = (
446
+ f"%s thing -o {self.filename} --depth 1; cat {self.filename}"
447
+ % self.envstack_bin
448
+ )
449
+ expected_output = """#!/usr/bin/env envstack
450
+ include: [default]
451
+ all: &all
452
+ <<: *all
453
+ CHAR_LIST: [a, b, c, "${HELLO}"]
454
+ DICT: {a: 1, b: 2, c: "${INT}"}
455
+ FLOAT: 1.0
456
+ HELLO: goodbye
457
+ INT: 5
458
+ LOG_LEVEL: ${LOG_LEVEL:=INFO}
459
+ NUMBER_LIST: [1, 2, 3]
460
+ darwin:
461
+ <<: *all
462
+ ROOT: ${HOME}/Library/Application Support/pipe
463
+ linux:
464
+ <<: *all
465
+ ROOT: ${HOME}/.local/pipe
466
+ windows:
467
+ <<: *all
468
+ ROOT: C:/ProgramData/pipe
469
+ """
470
+ output = subprocess.check_output(
471
+ command,
472
+ shell=True,
473
+ universal_newlines=True,
474
+ )
475
+ self.assertEqual(output, expected_output)
476
+
477
+ def test_default_encrypted(self):
478
+ """Tests baking the default stack encrypted with base64."""
479
+ command = (
480
+ f"%s --encrypt -o {self.filename}; cat {self.filename}" % self.envstack_bin
481
+ )
482
+ expected_output = """#!/usr/bin/env envstack
483
+ include: []
484
+ all: &all
485
+ <<: *all
486
+ DEPLOY_ROOT: !encrypt JHtST09UfS8ke0VOVn0=
487
+ ENV: !encrypt cHJvZA==
488
+ ENVPATH: !encrypt JHtERVBMT1lfUk9PVH0vZW52OiR7RU5WUEFUSH0=
489
+ HELLO: !encrypt JHtIRUxMTzo9d29ybGR9
490
+ LOG_LEVEL: !encrypt JHtMT0dfTEVWRUw6PUlORk99
491
+ PATH: !encrypt JHtERVBMT1lfUk9PVH0vYmluOiR7UEFUSH0=
492
+ PYTHONPATH: !encrypt JHtERVBMT1lfUk9PVH0vbGliL3B5dGhvbjoke1BZVEhPTlBBVEh9
493
+ darwin:
494
+ <<: *all
495
+ ROOT: !encrypt L1ZvbHVtZXMvcGlwZQ==
496
+ linux:
497
+ <<: *all
498
+ ROOT: !encrypt L21udC9waXBl
499
+ windows:
500
+ <<: *all
501
+ ROOT: !encrypt WDovcGlwZQ==
502
+ """
503
+ output = subprocess.check_output(
504
+ command,
505
+ shell=True,
506
+ universal_newlines=True,
507
+ )
508
+ self.assertEqual(output, expected_output)
509
+
510
+ def test_thing_encrypted(self):
511
+ """Tests baking the thing stack with depth of 1 excrypted."""
512
+ command = (
513
+ f"%s thing --encrypt -o {self.filename} --depth 1; cat {self.filename}"
514
+ % self.envstack_bin
515
+ )
516
+ expected_output = """#!/usr/bin/env envstack
517
+ include: [default]
518
+ all: &all
519
+ <<: *all
520
+ CHAR_LIST: !encrypt WydhJywgJ2InLCAnYycsICcke0hFTExPfSdd
521
+ DICT: !encrypt eydhJzogMSwgJ2InOiAyLCAnYyc6ICcke0lOVH0nfQ==
522
+ FLOAT: !encrypt MS4w
523
+ HELLO: !encrypt Z29vZGJ5ZQ==
524
+ INT: !encrypt NQ==
525
+ LOG_LEVEL: !encrypt JHtMT0dfTEVWRUw6PUlORk99
526
+ NUMBER_LIST: !encrypt WzEsIDIsIDNd
527
+ darwin:
528
+ <<: *all
529
+ ROOT: !encrypt JHtIT01FfS9MaWJyYXJ5L0FwcGxpY2F0aW9uIFN1cHBvcnQvcGlwZQ==
530
+ linux:
531
+ <<: *all
532
+ ROOT: !encrypt JHtIT01FfS8ubG9jYWwvcGlwZQ==
533
+ windows:
534
+ <<: *all
535
+ ROOT: !encrypt QzovUHJvZ3JhbURhdGEvcGlwZQ==
536
+ """
537
+ output = subprocess.check_output(
538
+ command,
539
+ shell=True,
540
+ universal_newlines=True,
541
+ )
542
+ self.assertEqual(output, expected_output)
543
+
544
+ def test_blank(self):
545
+ """Tests baking a blank stack."""
546
+ command = (
547
+ f"%s doesnotexist -o {self.filename}; cat {self.filename}"
548
+ % self.envstack_bin
549
+ )
550
+ expected_output = """#!/usr/bin/env envstack
551
+ include: []
552
+ all: &all
553
+ <<: *all
554
+ darwin:
555
+ <<: *all
556
+ linux:
557
+ <<: *all
558
+ windows:
559
+ <<: *all
560
+ """
561
+ output = subprocess.check_output(
562
+ command,
563
+ shell=True,
564
+ universal_newlines=True,
565
+ )
566
+ self.assertEqual(output, expected_output)
567
+
568
+
353
569
  class TestCommands(unittest.TestCase):
354
570
  """Tests various envstack commands."""
355
571
 
@@ -583,4 +799,8 @@ class TestIssues(unittest.TestCase):
583
799
 
584
800
 
585
801
  if __name__ == "__main__":
586
- unittest.main()
802
+ # TODO: make tests_cmds.py cross-platform
803
+ if sys.platform == "linux":
804
+ unittest.main()
805
+ else:
806
+ print(f"platform not supported: {sys.platform}")
@@ -214,7 +214,7 @@ class TestInit(unittest.TestCase):
214
214
  self.assertEqual(os.getenv("ROOT"), self.root)
215
215
  self.assertEqual(os.getenv("DEPLOY_ROOT"), f"{self.root}/prod")
216
216
  self.assertTrue(len(sys.path) > len(sys_path))
217
- self.assertTrue(len(os.getenv("PATH")) > len(path))
217
+ # self.assertTrue(len(os.getenv("PATH")) > len(path))
218
218
  self.assertTrue("prod/lib/python" in os.getenv("PYTHONPATH"))
219
219
  self.assertTrue("prod/bin" in os.getenv("PATH"))
220
220
 
@@ -277,6 +277,142 @@ class TestInit(unittest.TestCase):
277
277
  self.assertEqual(diffs["unchanged"], original_env)
278
278
 
279
279
 
280
+ class TestResolveEnviron(unittest.TestCase):
281
+ """Tests the resolve_environ function."""
282
+
283
+ def test_simple(self):
284
+ """Tests resolving a simple environment."""
285
+ from envstack.env import resolve_environ
286
+
287
+ env = {"FOO": "foo", "BAR": "${FOO}"}
288
+ resolved = resolve_environ(env)
289
+ self.assertEqual(resolved["BAR"], "foo")
290
+
291
+ def test_nested(self):
292
+ """Tests resolving a nested environment."""
293
+ from envstack.env import resolve_environ
294
+
295
+ env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": "${BAR}"}
296
+ resolved = resolve_environ(env)
297
+ self.assertEqual(resolved["BAZ"], "foo")
298
+
299
+ def test_recursive(self):
300
+ """Tests resolving a recursive environment."""
301
+ from envstack.env import resolve_environ
302
+
303
+ env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": "${BAR}", "QUX": "${BAZ}"}
304
+ resolved = resolve_environ(env)
305
+ self.assertEqual(resolved["QUX"], "foo")
306
+
307
+ def test_recursive_list(self):
308
+ """Tests resolving a recursive list environment."""
309
+ from envstack.env import resolve_environ
310
+
311
+ env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": ["${BAR}"]}
312
+ resolved = resolve_environ(env)
313
+ self.assertEqual(resolved["BAZ"], ["foo"])
314
+
315
+ def test_recursive_dict(self):
316
+ """Tests resolving a recursive dict environment."""
317
+ from envstack.env import resolve_environ
318
+
319
+ env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": {"qux": "${BAR}"}}
320
+ resolved = resolve_environ(env)
321
+ self.assertEqual(resolved["BAZ"], {"qux": "foo"})
322
+
323
+ def test_expansion_modifier(self):
324
+ """Tests var with an expansion modifier."""
325
+ from envstack.env import resolve_environ
326
+
327
+ env = {"VAR": "${VAR:=/foo/bar}"}
328
+ resolved = resolve_environ(env)
329
+ self.assertEqual(resolved["VAR"], "/foo/bar")
330
+
331
+ def test_expansion_modifier_nested_undefined(self):
332
+ """Tests expansion modifier with no value."""
333
+ from envstack.env import resolve_environ
334
+
335
+ env = {"VAR": "${VAR:=${FOO}}"}
336
+ resolved = resolve_environ(env)
337
+ self.assertEqual(resolved["VAR"], "")
338
+
339
+ def test_expansion_modifier_nested_default(self):
340
+ """Tests expansion modifier with default value."""
341
+ from envstack.env import resolve_environ
342
+
343
+ env = {"VAR": "${VAR:=${FOO:=bar}}"}
344
+ resolved = resolve_environ(env)
345
+ self.assertEqual(resolved["VAR"], "bar")
346
+
347
+ def test_expansion_modifier_nested_default_slash(self):
348
+ """Tests expansion modifier with special chars."""
349
+ from envstack.env import resolve_environ
350
+
351
+ env = {"VAR": "${VAR:=${FOO:=/foo/bar}}"}
352
+ resolved = resolve_environ(env)
353
+ self.assertEqual(resolved["VAR"], "/foo/bar")
354
+
355
+ def test_expansion_modifier_deferred(self):
356
+ """Tests expansion modifier with deferred value."""
357
+ from envstack.env import resolve_environ
358
+
359
+ env = {"VAR": "${VAR:=${FOO}}", "FOO": "${FOO:=/foo/bar}"}
360
+ env["BAR"] = "${BAZ}"
361
+ resolved = resolve_environ(env)
362
+ self.assertEqual(resolved["VAR"], "/foo/bar")
363
+ self.assertEqual(resolved["FOO"], "/foo/bar")
364
+
365
+ def test_expansion_modifier_deferred_default(self):
366
+ """Tests expansion modifier with multiple deferred values."""
367
+ from envstack.env import resolve_environ
368
+
369
+ env = {"VAR": "${VAR:=${FOO}}", "FOO": "${FOO:=${BAR}}"}
370
+ env["BAR"] = "${BAZ:=/baz/qux}" # insert a default value
371
+ resolved = resolve_environ(env)
372
+ self.assertEqual(resolved["VAR"], "/baz/qux")
373
+ self.assertEqual(resolved["FOO"], "/baz/qux")
374
+ self.assertEqual(resolved["BAR"], "/baz/qux")
375
+
376
+ def test_expansion_modifier_deferred_default_slash(self):
377
+ """Tests expansion modifier with multiple deferred values."""
378
+ from envstack.env import resolve_environ
379
+
380
+ env = {"VAR": "${VAR:=${FOO}}", "FOO": "${FOO:=${BAR}}"}
381
+ env["BAR"] = "${BAZ:=/baz/qux}"
382
+ env["BAZ"] = "${QUX}"
383
+ resolved = resolve_environ(env)
384
+ self.assertEqual(resolved["VAR"], "/baz/qux")
385
+ self.assertEqual(resolved["FOO"], "/baz/qux")
386
+ self.assertEqual(resolved["BAR"], "/baz/qux")
387
+ self.assertEqual(resolved["BAZ"], "")
388
+ self.assertRaises(KeyError, lambda: resolved["QUX"])
389
+
390
+ # FIXME: these tests are not passing
391
+ # def test_recursive_list_dict(self):
392
+ # """Tests resolving a recursive list dict environment."""
393
+ # from envstack.env import resolve_environ
394
+
395
+ # env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": [{"qux": "${BAR}"}]}
396
+ # resolved = resolve_environ(env)
397
+ # self.assertEqual(resolved["BAZ"], [{"qux": "foo"}])
398
+
399
+ # def test_recursive_dict_list(self):
400
+ # """Tests resolving a recursive dict list environment."""
401
+ # from envstack.env import resolve_environ
402
+
403
+ # env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": {"qux": ["${BAR}"]}}
404
+ # resolved = resolve_environ(env)
405
+ # self.assertEqual(resolved["BAZ"], {"qux": ["foo"]})
406
+
407
+ # def test_recursive_list_list(self):
408
+ # """Tests resolving a recursive list list environment."""
409
+ # from envstack.env import resolve_environ
410
+
411
+ # env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": [["${BAR}"]]}
412
+ # resolved = resolve_environ(env)
413
+ # self.assertEqual(resolved["BAZ"], [["foo"]])
414
+
415
+
280
416
  class TestBakeEnviron(unittest.TestCase):
281
417
  def setUp(self):
282
418
  self.root = create_test_root()
@@ -292,16 +428,29 @@ class TestBakeEnviron(unittest.TestCase):
292
428
  """Bakes a given stack and compares values."""
293
429
  from envstack.env import bake_environ, load_environ
294
430
 
295
- default = load_environ(stack_name)
431
+ env = load_environ(stack_name)
296
432
  envstack.revert() # FIXME: revert should not be required
297
433
  baked = bake_environ(stack_name)
298
434
 
299
435
  # make sure environment sources are different
300
- self.assertNotEqual(default.sources, baked.sources)
301
- self.assertTrue(len(default) > 0)
302
- self.assertTrue(len(baked) > 0)
303
-
304
- for key, value in default.items():
436
+ if stack_name == "doesnotexist":
437
+ self.assertEqual(env.sources, [])
438
+ self.assertEqual(baked.sources, [])
439
+ elif stack_name == "default":
440
+ self.assertTrue(len(env.sources[0].includes()) == 0)
441
+ else:
442
+ self.assertNotEqual(env.sources, baked.sources)
443
+ self.assertTrue(len(env.sources) > 0)
444
+ self.assertEqual(baked.sources, [])
445
+ self.assertTrue(len(env) > 0)
446
+ self.assertTrue(len(baked) > 0)
447
+
448
+ # "include" key should not be present
449
+ self.assertTrue("include" not in env)
450
+ self.assertTrue("include" not in baked)
451
+
452
+ # compare the values
453
+ for key, value in env.items():
305
454
  if key == "STACK": # skip the stack name
306
455
  continue
307
456
  self.assertEqual(baked[key], value)
@@ -314,11 +463,22 @@ class TestBakeEnviron(unittest.TestCase):
314
463
 
315
464
  envstack.revert() # FIXME: revert should not be required
316
465
  baked2_reloaded = load_environ("baked")
317
- self.assertNotEqual(baked2.sources, baked2_reloaded.sources)
318
- self.assertTrue(len(baked2) > 0)
319
- self.assertTrue(len(baked2_reloaded) > 0)
320
466
 
321
- for key, value in baked.items():
467
+ # make sure environment sources are different
468
+ if stack_name == "doesnotexist":
469
+ self.assertEqual(env.sources, [])
470
+ self.assertEqual(baked2.sources, [])
471
+ else:
472
+ self.assertNotEqual(baked2.sources, baked2_reloaded.sources)
473
+ self.assertTrue(len(baked2) > 0)
474
+ self.assertTrue(len(baked2_reloaded) > 0)
475
+
476
+ # "include" key should not be present
477
+ self.assertTrue("include" not in env)
478
+ self.assertTrue("include" not in baked2)
479
+
480
+ # compare the values
481
+ for key, value in env.items():
322
482
  if key == "STACK":
323
483
  continue
324
484
  self.assertEqual(baked2_reloaded[key], value)
@@ -330,6 +490,10 @@ class TestBakeEnviron(unittest.TestCase):
330
490
  """Tests baking the default environment."""
331
491
  self.bake_environ("default")
332
492
 
493
+ def test_bake_empty(self):
494
+ """Tests baking new environment."""
495
+ self.bake_environ("doesnotexist")
496
+
333
497
  def test_bake_dev(self):
334
498
  """Tests baking the dev environment."""
335
499
  self.bake_environ("dev")
@@ -372,6 +536,10 @@ class TestEncryptEnviron(unittest.TestCase):
372
536
  self.assertTrue(len(env) > 0)
373
537
  self.assertTrue(len(encrypted) > 0)
374
538
 
539
+ # "include" key should not be present
540
+ self.assertTrue("include" not in env)
541
+ self.assertTrue("include" not in encrypted)
542
+
375
543
  for key, value in env.items():
376
544
  if key == "STACK": # skip the stack name
377
545
  continue
@@ -417,6 +585,10 @@ class TestEncryptEnviron(unittest.TestCase):
417
585
  self.assertTrue(len(default) > 0)
418
586
  self.assertTrue(len(encrypted) > 0)
419
587
 
588
+ # "include" key should not be present
589
+ self.assertTrue("include" not in default)
590
+ self.assertTrue("include" not in encrypted)
591
+
420
592
  for key, value in default.items():
421
593
  if key == "STACK": # skip the stack name
422
594
  continue
@@ -39,6 +39,7 @@ import unittest
39
39
  from envstack import config
40
40
  from envstack.exceptions import CyclicalReference
41
41
  from envstack.util import (
42
+ null,
42
43
  encode,
43
44
  evaluate_modifiers,
44
45
  get_stack_name,
@@ -48,60 +49,199 @@ from envstack.util import (
48
49
 
49
50
 
50
51
  class TestEvaluateModifiers(unittest.TestCase):
52
+ """Tests for evaluate_modifiers function."""
53
+
51
54
  def test_no_substitution(self):
55
+ """Test no substitution."""
52
56
  expression = "world"
53
57
  result = evaluate_modifiers(expression)
54
58
  self.assertEqual(result, "world")
55
59
 
56
60
  def test_direct_substitution(self):
61
+ """Test direct substitution."""
57
62
  expression = "${VAR}"
58
63
  environ = {"VAR": "hello"}
59
64
  result = evaluate_modifiers(expression, environ)
60
65
  self.assertEqual(result, "hello")
61
66
 
67
+ def test_default_null_value(self):
68
+ """Test var null value."""
69
+ expression = "${VAR}"
70
+ environ = {}
71
+ result = evaluate_modifiers(expression, environ)
72
+ self.assertEqual(result, null)
73
+
62
74
  def test_default_value(self):
75
+ """Test default value."""
63
76
  expression = "${VAR:=default}"
64
77
  environ = {"VAR": "hello"}
65
78
  result = evaluate_modifiers(expression, environ)
66
79
  self.assertEqual(result, "hello")
67
80
 
68
81
  def test_default_value_empty_env(self):
82
+ """Test default value with empty environment."""
69
83
  expression = "${VAR:=default}"
70
84
  environ = {}
71
85
  result = evaluate_modifiers(expression, environ)
72
86
  self.assertEqual(result, "default")
73
87
 
88
+ def test_pathlike_value(self):
89
+ """Test path-like value with two vars."""
90
+ expression = "${ROOT}/${ENV}"
91
+ environ = {"ROOT": "/usr/local", "ENV": "env"}
92
+ result = evaluate_modifiers(expression, environ)
93
+ self.assertEqual(result, "/usr/local/env")
94
+
95
+ def test_pathlike_value_colon_separated(self):
96
+ """Test colon-separated path-like value with two vars."""
97
+ expression = "${DEPLOY_ROOT}/env:${ENVPATH}"
98
+ environ = {"DEPLOY_ROOT": "/usr/local/lib", "ENVPATH": "/mnt/env"}
99
+ result = evaluate_modifiers(expression, environ)
100
+ self.assertEqual(result, f"/usr/local/lib/env{os.pathsep}/mnt/env")
101
+
74
102
  def test_default_value_with_default_args(self):
103
+ """Test default value with default args."""
75
104
  expression = "${HELLO:=world}"
76
105
  result = evaluate_modifiers(expression)
77
106
  self.assertEqual(result, os.getenv("HELLO", "world"))
78
107
 
79
108
  def test_error_message(self):
109
+ """Test error message."""
80
110
  expression = "${VAR:?error message}"
81
111
  environ = {"VAR": "hello"}
82
112
  result = evaluate_modifiers(expression, environ)
83
113
  self.assertEqual(result, "hello")
84
114
 
85
115
  def test_error_message_raise(self):
116
+ """Test error message raise."""
86
117
  expression = "${VAR:?error message}"
87
118
  environ = {}
88
119
  with self.assertRaises(ValueError):
89
120
  evaluate_modifiers(expression, environ)
90
121
 
91
122
  def test_cyclical_reference_error(self):
123
+ """Test cyclical reference error."""
92
124
  expression = "${VAR}"
93
125
  environ = {"VAR": "${FOO}", "FOO": "${BAR}", "BAR": "${VAR}"}
94
126
  with self.assertRaises(CyclicalReference):
95
127
  evaluate_modifiers(expression, environ)
96
128
 
97
129
  def test_multiple_substitutions(self):
130
+ """Test multiple substitutions."""
98
131
  expression = "${VAR}/${FOO:=foobar}/${BAR:?error message}"
99
132
  environ = {"VAR": "hello", "BAR": "world"}
100
133
  result = evaluate_modifiers(expression, environ)
101
134
  self.assertEqual(result, "hello/foobar/world")
102
135
 
136
+ def test_embedded_substitution(self):
137
+ """Test embedded substitution with default."""
138
+ expression = "${VAR:=${FOO:=bar}}"
139
+ environ = {"FOO": "foo"}
140
+ result = evaluate_modifiers(expression, environ)
141
+ self.assertEqual(result, "foo")
142
+
143
+ def test_embedded_substitution_default(self):
144
+ """Test embedded substitution with default."""
145
+ expression = "${VAR:=${FOO:=bar}}"
146
+ environ = {}
147
+ result = evaluate_modifiers(expression, environ)
148
+ self.assertEqual(result, "bar")
149
+
150
+ def test_embedded_substitution_value_var(self):
151
+ """Test embedded substitution with value for VAR."""
152
+ expression = "${VAR:=${FOO}}"
153
+ environ = {"VAR": "foobar"}
154
+ result = evaluate_modifiers(expression, environ)
155
+ self.assertEqual(result, "foobar")
156
+
157
+ def test_embedded_substitution_value_var_foo(self):
158
+ """Test embedded substitution with values for VAR and FOO."""
159
+ expression = "${VAR:=${FOO}}"
160
+ environ = {"VAR": "foobar", "FOO": "barfoo"}
161
+ result = evaluate_modifiers(expression, environ)
162
+ self.assertEqual(result, "foobar")
163
+
164
+ def test_embedded_substitution_value_foo(self):
165
+ """Test embedded substitution with value for FOO."""
166
+ expression = "${VAR:=${FOO}}"
167
+ environ = {"FOO": "barfoo"}
168
+ result = evaluate_modifiers(expression, environ)
169
+ self.assertEqual(result, "barfoo")
170
+
171
+ def test_embedded_substitution_value_var_slashes(self):
172
+ """Test embedded substitution with value with special chars."""
173
+ expression = "${VAR:=${FOO}}"
174
+ environ = {"VAR": "/foo/bar"}
175
+ result = evaluate_modifiers(expression, environ)
176
+ self.assertEqual(result, "/foo/bar")
177
+
178
+ def test_embedded_substitution_value_bar_slashes(self):
179
+ """Test embedded substitution with value with special chars."""
180
+ expression = "${VAR:=${FOO}}"
181
+ environ = {"FOO": "/bar/foo"}
182
+ result = evaluate_modifiers(expression, environ)
183
+ self.assertEqual(result, "/bar/foo")
184
+
185
+ def test_embedded_substitution_multiple_one(self):
186
+ """Test multiple embedded substitution."""
187
+ expression = "${VAR:=${FOO:=${BAR}}}"
188
+ environ = {"BAR": "/foo/bar"}
189
+ result = evaluate_modifiers(expression, environ)
190
+ self.assertEqual(result, "/foo/bar")
191
+
192
+ def test_embedded_substitution_multiple_two(self):
193
+ """Test multiple embedded substitution with value."""
194
+ expression = "${VAR:=${FOO:=${BAR}}}"
195
+ environ = {"FOO": "/test/a/b/c"}
196
+ result = evaluate_modifiers(expression, environ)
197
+ self.assertEqual(result, "/test/a/b/c")
198
+
199
+ def test_embedded_substitution_multiple_three(self):
200
+ """Test multiple embedded substitution with value."""
201
+ expression = "${VAR:=${FOO:=${BAR}}}"
202
+ environ = {"VAR": "/test/x/y/z"}
203
+ result = evaluate_modifiers(expression, environ)
204
+ self.assertEqual(result, "/test/x/y/z")
205
+
206
+ def test_embedded_substitution_multiple_default(self):
207
+ """Test multiple embedded substitution with default value."""
208
+ expression = "${VAR:=${FOO:=${BAR:=/foo/bar/baz}}}"
209
+ environ = {}
210
+ result = evaluate_modifiers(expression, environ)
211
+ self.assertEqual(result, "/foo/bar/baz")
212
+
213
+ def test_embedded_substitution_prefix(self):
214
+ """Test embedded substitution with prefix."""
215
+ expression = "${VAR:=default}/path"
216
+ environ = {"VAR": "value"}
217
+ result = evaluate_modifiers(expression, environ)
218
+ self.assertEqual(result, "value/path")
219
+
220
+ def test_embedded_substitution_prefix_default(self):
221
+ """Test embedded substitution with prefix with default."""
222
+ expression = "${VAR:=default}/path"
223
+ environ = {}
224
+ result = evaluate_modifiers(expression, environ)
225
+ self.assertEqual(result, "default/path")
226
+
227
+ def test_embedded_substitution_with_slash(self):
228
+ """Test embedded substitution with special char /."""
229
+ expression = "${VAR:=${FOO}/bar}}"
230
+ environ = {"FOO": "foo"}
231
+ result = evaluate_modifiers(expression, environ)
232
+ self.assertEqual(result, "foo/bar")
233
+
234
+ def test_embedded_substitution_with_hyphen(self):
235
+ """Test embedded substitution with special char -."""
236
+ expression = "${VAR:=${FOO}-bar}}"
237
+ environ = {"FOO": "foo"}
238
+ result = evaluate_modifiers(expression, environ)
239
+ self.assertEqual(result, "foo-bar")
240
+
103
241
 
104
242
  class TestUtils(unittest.TestCase):
243
+ """Tests for util.py module."""
244
+
105
245
  def test_encode(self):
106
246
  env = {
107
247
  "VAR1": "value1",
@@ -173,6 +313,8 @@ darwin:
173
313
 
174
314
 
175
315
  class TestDedupePaths(unittest.TestCase):
316
+ """Tests for dedupe_paths function."""
317
+
176
318
  def test_dedupe_list(self):
177
319
  """Test dedupe_list function."""
178
320
  from envstack.util import dedupe_list
@@ -269,6 +411,8 @@ class TestDedupePaths(unittest.TestCase):
269
411
 
270
412
 
271
413
  class TestSafeEval(unittest.TestCase):
414
+ """Tests for safe_eval function."""
415
+
272
416
  def test_safe_eval_string(self):
273
417
  value = "hello"
274
418
  result = safe_eval(value)
@@ -301,8 +445,54 @@ class TestSafeEval(unittest.TestCase):
301
445
 
302
446
 
303
447
  class TestPartitionPlatformData(unittest.TestCase):
448
+ """Tests for partition_platform_data function."""
449
+
450
+ def test_partition_platform_data_empty(self):
451
+ """Test partition_platform_data with empty data."""
452
+ data = {}
453
+ result = partition_platform_data(data)
454
+ expected_result = {
455
+ "include": [],
456
+ "all": {
457
+ "<<": "*all",
458
+ },
459
+ "darwin": {
460
+ "<<": "*all",
461
+ },
462
+ "linux": {
463
+ "<<": "*all",
464
+ },
465
+ "windows": {
466
+ "<<": "*all",
467
+ },
468
+ }
469
+ self.assertEqual(result, expected_result)
470
+
471
+ def test_partition_platform_data_empty_includes(self):
472
+ """Test partition_platform_data with empty includes."""
473
+ data = {"include": []}
474
+ result = partition_platform_data(data)
475
+ expected_result = {
476
+ "include": [],
477
+ "all": {
478
+ "<<": "*all",
479
+ },
480
+ "darwin": {
481
+ "<<": "*all",
482
+ },
483
+ "linux": {
484
+ "<<": "*all",
485
+ },
486
+ "windows": {
487
+ "<<": "*all",
488
+ },
489
+ }
490
+ self.assertEqual(result, expected_result)
491
+
304
492
  def test_partition_platform_data(self):
493
+ """Test partition_platform_data with data."""
305
494
  data = {
495
+ "include": [],
306
496
  "all": {
307
497
  "key1": "value1",
308
498
  "key2": "value2",
@@ -329,6 +519,7 @@ class TestPartitionPlatformData(unittest.TestCase):
329
519
  }
330
520
 
331
521
  expected_result = {
522
+ "include": [],
332
523
  "all": {
333
524
  "<<": "*all",
334
525
  "key1": "value1",
@@ -357,6 +548,8 @@ class TestPartitionPlatformData(unittest.TestCase):
357
548
 
358
549
 
359
550
  class TestIssue18(unittest.TestCase):
551
+ """Tests for issue #18."""
552
+
360
553
  def test_non_cyclical_reference_error_1(self):
361
554
  expression = "${FOO}"
362
555
  environ = {"FOO": "${FOO}"}
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