envstack 0.8.2__tar.gz → 0.8.3__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.2/lib/envstack.egg-info → envstack-0.8.3}/PKG-INFO +14 -1
  2. {envstack-0.8.2 → envstack-0.8.3}/README.md +13 -0
  3. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/__init__.py +1 -1
  4. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/util.py +20 -31
  5. {envstack-0.8.2 → envstack-0.8.3/lib/envstack.egg-info}/PKG-INFO +14 -1
  6. {envstack-0.8.2 → envstack-0.8.3}/setup.py +1 -1
  7. {envstack-0.8.2 → envstack-0.8.3}/tests/test_cmds.py +1 -5
  8. {envstack-0.8.2 → envstack-0.8.3}/tests/test_env.py +109 -19
  9. {envstack-0.8.2 → envstack-0.8.3}/tests/test_util.py +71 -8
  10. {envstack-0.8.2 → envstack-0.8.3}/LICENSE +0 -0
  11. {envstack-0.8.2 → envstack-0.8.3}/dist.json +0 -0
  12. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/cli.py +0 -0
  13. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/config.py +0 -0
  14. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/encrypt.py +0 -0
  15. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/env.py +0 -0
  16. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/exceptions.py +0 -0
  17. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/logger.py +0 -0
  18. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/node.py +0 -0
  19. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/path.py +0 -0
  20. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack/wrapper.py +0 -0
  21. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack.egg-info/SOURCES.txt +0 -0
  22. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack.egg-info/dependency_links.txt +0 -0
  23. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack.egg-info/entry_points.txt +0 -0
  24. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack.egg-info/not-zip-safe +0 -0
  25. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack.egg-info/requires.txt +0 -0
  26. {envstack-0.8.2 → envstack-0.8.3}/lib/envstack.egg-info/top_level.txt +0 -0
  27. {envstack-0.8.2 → envstack-0.8.3}/setup.cfg +0 -0
  28. {envstack-0.8.2 → envstack-0.8.3}/tests/test_encrypt.py +0 -0
  29. {envstack-0.8.2 → envstack-0.8.3}/tests/test_node.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: envstack
3
- Version: 0.8.2
3
+ Version: 0.8.3
4
4
  Summary: Stacked environment variable management system
5
5
  Home-page: http://github.com/rsgalloway/envstack
6
6
  Author: Ryan Galloway
@@ -277,6 +277,19 @@ BIZ=foo
277
277
  FOO=foo
278
278
  ```
279
279
 
280
+ Here is an example using nested variable expansion:
281
+
282
+ ```yaml
283
+ FOO: ${BIZ:=${BAR:=${BAZ:=baz}}}
284
+ ```
285
+
286
+ Resolves to:
287
+
288
+ ```bash
289
+ $ envstack -r
290
+ FOO=baz
291
+ ```
292
+
280
293
  #### Includes
281
294
 
282
295
  Environment stack files can include other namespaced environments (you should
@@ -253,6 +253,19 @@ BIZ=foo
253
253
  FOO=foo
254
254
  ```
255
255
 
256
+ Here is an example using nested variable expansion:
257
+
258
+ ```yaml
259
+ FOO: ${BIZ:=${BAR:=${BAZ:=baz}}}
260
+ ```
261
+
262
+ Resolves to:
263
+
264
+ ```bash
265
+ $ envstack -r
266
+ FOO=baz
267
+ ```
268
+
256
269
  #### Includes
257
270
 
258
271
  Environment stack files can include other namespaced environments (you should
@@ -34,6 +34,6 @@ Stacked environment variable management system.
34
34
  """
35
35
 
36
36
  __prog__ = "envstack"
37
- __version__ = "0.8.2"
37
+ __version__ = "0.8.3"
38
38
 
39
39
  from envstack.env import clear, init, revert, save # noqa: F401
@@ -49,14 +49,14 @@ from envstack.node import AESGCMNode, Base64Node, EncryptedNode, FernetNode
49
49
  # value for unresolvable variables
50
50
  null = ""
51
51
 
52
+ # regular expression pattern for matching windows drive letters
53
+ drive_letter_pattern = re.compile(r"(?P<sep>[:;])?(?P<drive>[A-Z]:[/\\])")
54
+
52
55
  # regular expression pattern for bash-like variable expansion
53
56
  variable_pattern = re.compile(
54
- r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([=?])(\$\{[^}]*\}[\-\w/]*|[^}]*))?\}"
57
+ r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([=?])((?:\$\{[^}]+\}|[^}])*))?\}"
55
58
  )
56
59
 
57
- # regular expression pattern for matching windows drive letters
58
- drive_letter_pattern = re.compile(r"(?P<sep>[:;])?(?P<drive>[A-Z]:[/\\])")
59
-
60
60
 
61
61
  def clear_sys_path(var: str = "PYTHONPATH"):
62
62
  """
@@ -72,9 +72,9 @@ def clear_sys_path(var: str = "PYTHONPATH"):
72
72
  def decode_value(value: str):
73
73
  """Returns a decoded value that's been encoded by a wrapper.
74
74
 
75
- Decoding encoded environments can be tricky. For example, it must account for path
76
- templates that include curly braces, e.g. path templates string like this must be
77
- preserved:
75
+ Decoding encoded environments can be tricky. For example, it must account
76
+ for path templates that include curly braces, e.g. path templates string
77
+ like this must be preserved:
78
78
 
79
79
  '/path/with/{variable}'
80
80
 
@@ -270,6 +270,16 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
270
270
  error message.
271
271
  """
272
272
 
273
+ def sanitize_value(value):
274
+ """Sanitize the value before returning it."""
275
+ # HACK: remove trailing curly braces if they exist
276
+ if type(value) is str and value.endswith("}") and not value.startswith("${"):
277
+ return value.rstrip("}")
278
+ # sanitize path-like values
279
+ elif type(value) is str and ":" in value and ("/" in value or "\\" in value):
280
+ return dedupe_paths(value)
281
+ return value
282
+
273
283
  def substitute_variable(match):
274
284
  """Substitute a variable match with its value."""
275
285
  var_name = match.group(1)
@@ -311,19 +321,10 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
311
321
  # substitute all matches in the expression
312
322
  result = variable_pattern.sub(substitute_variable, expression)
313
323
 
314
- # HACK: remove trailing curly braces if they exist
315
- if result.endswith("}") and not result.startswith("${"):
316
- result = result.rstrip("}")
317
-
318
324
  # evaluate any remaining modifiers, eg. ${VAR:=${FOO:=bar}}
319
325
  if variable_pattern.search(result):
320
326
  result = evaluate_modifiers(result, environ)
321
327
 
322
- # dedupe path-like values and resolve separators
323
- # TODO: replace with regex pattern to detect path-like strings
324
- elif ":" in result and ("/" in result or "\\" in result):
325
- result = dedupe_paths(result)
326
-
327
328
  # detect recursion errors
328
329
  except RecursionError:
329
330
  raise CyclicalReference(f"Cyclical reference detected in {expression}")
@@ -340,27 +341,15 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
340
341
  elif isinstance(expression, FernetNode):
341
342
  result = expression.resolve(env=environ)
342
343
  elif isinstance(expression, list):
343
- result = [
344
- (
345
- variable_pattern.sub(substitute_variable, str(v))
346
- if isinstance(v, str)
347
- else v
348
- )
349
- for v in expression
350
- ]
344
+ result = [(evaluate_modifiers(v, environ)) for v in expression]
351
345
  elif isinstance(expression, dict):
352
346
  result = {
353
- k: (
354
- variable_pattern.sub(substitute_variable, str(v))
355
- if isinstance(v, str)
356
- else v
357
- )
358
- for k, v in expression.items()
347
+ k: (evaluate_modifiers(v, environ)) for k, v in expression.items()
359
348
  }
360
349
  else:
361
350
  result = expression
362
351
 
363
- return result
352
+ return sanitize_value(result)
364
353
 
365
354
 
366
355
  def load_sys_path(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: envstack
3
- Version: 0.8.2
3
+ Version: 0.8.3
4
4
  Summary: Stacked environment variable management system
5
5
  Home-page: http://github.com/rsgalloway/envstack
6
6
  Author: Ryan Galloway
@@ -277,6 +277,19 @@ BIZ=foo
277
277
  FOO=foo
278
278
  ```
279
279
 
280
+ Here is an example using nested variable expansion:
281
+
282
+ ```yaml
283
+ FOO: ${BIZ:=${BAR:=${BAZ:=baz}}}
284
+ ```
285
+
286
+ Resolves to:
287
+
288
+ ```bash
289
+ $ envstack -r
290
+ FOO=baz
291
+ ```
292
+
280
293
  #### Includes
281
294
 
282
295
  Environment stack files can include other namespaced environments (you should
@@ -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.2",
43
+ version="0.8.3",
44
44
  description="Stacked environment variable management system",
45
45
  long_description=long_description,
46
46
  long_description_content_type="text/markdown",
@@ -799,8 +799,4 @@ class TestIssues(unittest.TestCase):
799
799
 
800
800
 
801
801
  if __name__ == "__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}")
802
+ unittest.main()
@@ -348,10 +348,101 @@ class TestResolveEnviron(unittest.TestCase):
348
348
  """Tests expansion modifier with special chars."""
349
349
  from envstack.env import resolve_environ
350
350
 
351
+ # remove ENV and ROOT from the environment
352
+ if "FOO" in os.environ:
353
+ del os.environ["FOO"]
354
+ if "VAR" in os.environ:
355
+ del os.environ["VAR"]
351
356
  env = {"VAR": "${VAR:=${FOO:=/foo/bar}}"}
352
357
  resolved = resolve_environ(env)
353
358
  self.assertEqual(resolved["VAR"], "/foo/bar")
354
359
 
360
+ def test_deploy_root_two(self):
361
+ """Tests $DEPLOY_ROOT with two vars."""
362
+ from envstack.env import resolve_environ
363
+
364
+ # set ENV and ROOT in the environment
365
+ os.environ["ROOT"] = "/mnt/pipe"
366
+ os.environ["ENV"] = "dev"
367
+ env = {
368
+ "DEPLOY_ROOT": "${ROOT}/${ENV}}",
369
+ "ENV": "${ENV:=prod}",
370
+ "ROOT": "${ROOT:=/var/tmp}",
371
+ }
372
+ resolved = resolve_environ(env)
373
+ self.assertEqual(resolved["DEPLOY_ROOT"], "/mnt/pipe/dev")
374
+
375
+ def test_deploy_root_three(self):
376
+ """Tests $DEPLOY_ROOT with three vars."""
377
+ from envstack.env import resolve_environ
378
+
379
+ env_value = os.getenv("ENV", "prod")
380
+ env = {
381
+ "DEPLOY_ROOT": "${MOUNT}/${DRIVE}/${ENV}}",
382
+ "ENV": "${ENV:=prod}",
383
+ "MOUNT": "/mnt",
384
+ "DRIVE": "${DRIVE:=pipe}",
385
+ }
386
+ resolved = resolve_environ(env)
387
+ self.assertEqual(resolved["DEPLOY_ROOT"], f"/mnt/pipe/{env_value}")
388
+
389
+ def test_deploy_root_default_one(self):
390
+ """Tests $DEPLOY_ROOT with default value and one var."""
391
+ from envstack.env import resolve_environ
392
+
393
+ # remove DEPLOY_ROOT, ENV and ROOT from the environment
394
+ if "DEPLOY_ROOT" in os.environ:
395
+ del os.environ["DEPLOY_ROOT"]
396
+ if "ENV" in os.environ:
397
+ del os.environ["ENV"]
398
+ if "ROOT" in os.environ:
399
+ del os.environ["ROOT"]
400
+ env = {
401
+ "DEPLOY_ROOT": "${DEPLOY_ROOT:=${TMP}}",
402
+ "ENV": "${ENV:=prod}",
403
+ "ROOT": "${ROOT:=/var/tmp}",
404
+ "TMP": "${ROOT}/${ENV}",
405
+ }
406
+ resolved = resolve_environ(env)
407
+ self.assertEqual(resolved["DEPLOY_ROOT"], "/var/tmp/prod")
408
+
409
+ def test_deploy_root_default_two(self):
410
+ """Tests $DEPLOY_ROOT with default value and two vars."""
411
+ from envstack.env import resolve_environ
412
+
413
+ # remove DEPLOY_ROOT, ENV and ROOT from the environment
414
+ if "DEPLOY_ROOT" in os.environ:
415
+ del os.environ["DEPLOY_ROOT"]
416
+ if "ENV" in os.environ:
417
+ del os.environ["ENV"]
418
+ if "ROOT" in os.environ:
419
+ del os.environ["ROOT"]
420
+ env = {
421
+ "DEPLOY_ROOT": "${DEPLOY_ROOT:=${ROOT}/${ENV}}",
422
+ "ENV": "${ENV:=prod}",
423
+ "ROOT": "${ROOT:=/var/lib}",
424
+ }
425
+ resolved = resolve_environ(env)
426
+ self.assertEqual(resolved["DEPLOY_ROOT"], "/var/lib/prod")
427
+
428
+ def test_deploy_root_default_two_from_env(self):
429
+ """Tests $DEPLOY_ROOT with default value from env."""
430
+ from envstack.env import resolve_environ
431
+
432
+ # remove DEPLOY_ROOT, ENV and ROOT from the environment
433
+ if "ENV" in os.environ:
434
+ del os.environ["ENV"]
435
+ if "ROOT" in os.environ:
436
+ del os.environ["ROOT"]
437
+ os.environ["DEPLOY_ROOT"] = "/some/path/here"
438
+ env = {
439
+ "DEPLOY_ROOT": "${DEPLOY_ROOT:=${ROOT}/${ENV}}",
440
+ "ENV": "${ENV:=prod}",
441
+ "ROOT": "${ROOT:=/var/lib}",
442
+ }
443
+ resolved = resolve_environ(env)
444
+ self.assertEqual(resolved["DEPLOY_ROOT"], "/some/path/here")
445
+
355
446
  def test_expansion_modifier_deferred(self):
356
447
  """Tests expansion modifier with deferred value."""
357
448
  from envstack.env import resolve_environ
@@ -387,30 +478,29 @@ class TestResolveEnviron(unittest.TestCase):
387
478
  self.assertEqual(resolved["BAZ"], "")
388
479
  self.assertRaises(KeyError, lambda: resolved["QUX"])
389
480
 
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
481
+ def test_recursive_list_dict(self):
482
+ """Tests resolving a recursive list dict environment."""
483
+ from envstack.env import resolve_environ
394
484
 
395
- # env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": [{"qux": "${BAR}"}]}
396
- # resolved = resolve_environ(env)
397
- # self.assertEqual(resolved["BAZ"], [{"qux": "foo"}])
485
+ env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": [{"qux": "${BAR}"}]}
486
+ resolved = resolve_environ(env)
487
+ self.assertEqual(resolved["BAZ"], [{"qux": "foo"}])
398
488
 
399
- # def test_recursive_dict_list(self):
400
- # """Tests resolving a recursive dict list environment."""
401
- # from envstack.env import resolve_environ
489
+ def test_recursive_dict_list(self):
490
+ """Tests resolving a recursive dict list environment."""
491
+ from envstack.env import resolve_environ
402
492
 
403
- # env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": {"qux": ["${BAR}"]}}
404
- # resolved = resolve_environ(env)
405
- # self.assertEqual(resolved["BAZ"], {"qux": ["foo"]})
493
+ env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": {"qux": ["${BAR}"]}}
494
+ resolved = resolve_environ(env)
495
+ self.assertEqual(resolved["BAZ"], {"qux": ["foo"]})
406
496
 
407
- # def test_recursive_list_list(self):
408
- # """Tests resolving a recursive list list environment."""
409
- # from envstack.env import resolve_environ
497
+ def test_recursive_list_list(self):
498
+ """Tests resolving a recursive list list environment."""
499
+ from envstack.env import resolve_environ
410
500
 
411
- # env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": [["${BAR}"]]}
412
- # resolved = resolve_environ(env)
413
- # self.assertEqual(resolved["BAZ"], [["foo"]])
501
+ env = {"FOO": "foo", "BAR": "${FOO}", "BAZ": [["${BAR}"]]}
502
+ resolved = resolve_environ(env)
503
+ self.assertEqual(resolved["BAZ"], [["foo"]])
414
504
 
415
505
 
416
506
  class TestBakeEnviron(unittest.TestCase):
@@ -210,6 +210,13 @@ class TestEvaluateModifiers(unittest.TestCase):
210
210
  result = evaluate_modifiers(expression, environ)
211
211
  self.assertEqual(result, "/foo/bar/baz")
212
212
 
213
+ def test_embedded_substitution_multiple_env(self):
214
+ """Test multiple embedded substitution with value from env."""
215
+ expression = "${VAR:=${FOO:=${BAR:=/foo/bar/baz}}}"
216
+ environ = {"FOO": "/a/b/c"}
217
+ result = evaluate_modifiers(expression, environ)
218
+ self.assertEqual(result, "/a/b/c")
219
+
213
220
  def test_embedded_substitution_prefix(self):
214
221
  """Test embedded substitution with prefix."""
215
222
  expression = "${VAR:=default}/path"
@@ -224,19 +231,75 @@ class TestEvaluateModifiers(unittest.TestCase):
224
231
  result = evaluate_modifiers(expression, environ)
225
232
  self.assertEqual(result, "default/path")
226
233
 
227
- def test_embedded_substitution_with_slash(self):
228
- """Test embedded substitution with special char /."""
229
- expression = "${VAR:=${FOO}/bar}}"
234
+ def test_embedded_substitution_default_one_var_dash(self):
235
+ """Test embedded substitution with one var default dash"""
236
+ expression = "${VAR:=${FOO}-bar}"
230
237
  environ = {"FOO": "foo"}
231
238
  result = evaluate_modifiers(expression, environ)
232
- self.assertEqual(result, "foo/bar")
239
+ self.assertEqual(result, "foo-bar")
233
240
 
234
- def test_embedded_substitution_with_hyphen(self):
235
- """Test embedded substitution with special char -."""
236
- expression = "${VAR:=${FOO}-bar}}"
241
+ def test_embedded_substitution_default_one_var_slash(self):
242
+ """Test embedded substitution with one var default slash"""
243
+ expression = "${VAR:=${FOO}/bar}"
237
244
  environ = {"FOO": "foo"}
238
245
  result = evaluate_modifiers(expression, environ)
239
- self.assertEqual(result, "foo-bar")
246
+ self.assertEqual(result, "foo/bar")
247
+
248
+ def test_embedded_substitution_default_two_vars(self):
249
+ """Test embedded substitution with two var default."""
250
+ expression = "${VAR:=${FOO}/${BAR}}"
251
+ environ = {"FOO": "foo", "BAR": "bar"}
252
+ result = evaluate_modifiers(expression, environ)
253
+ self.assertEqual(result, "foo/bar")
254
+
255
+ def test_embedded_substitution_default_two_vars_alt_1(self):
256
+ """Test embedded substitution with two var default, alt 1."""
257
+ expression = "${VAR:=/${FOO}/${BAR:=bar}}"
258
+ environ = {"FOO": "foo", "BAR": "baz"}
259
+ result = evaluate_modifiers(expression, environ)
260
+ self.assertEqual(result, "/foo/baz")
261
+
262
+ def test_embedded_substitution_default_two_vars_alt_2(self):
263
+ """Test embedded substitution with two var default, alt 2."""
264
+ expression = "${VAR:=/${FOO}/${FOO}}"
265
+ environ = {"FOO": "foo", "BAR": "bar"}
266
+ result = evaluate_modifiers(expression, environ)
267
+ self.assertEqual(result, "/foo/foo")
268
+
269
+ def test_embedded_substitution_default_two_vars_alt_3(self):
270
+ """Test embedded substitution with two var default, alt 3."""
271
+ expression = "${VAR:=/${FOO}}/${FOO}"
272
+ environ = {"FOO": "foo", "BAR": "bar"}
273
+ result = evaluate_modifiers(expression, environ)
274
+ self.assertEqual(result, "/foo/foo")
275
+
276
+ def test_embedded_substitution_default_two_vars_alt_4(self):
277
+ """Test embedded substitution with two var default, alt 4."""
278
+ expression = "${VAR:=/${FOO}}/${BAR:=bar}"
279
+ environ = {"FOO": "foo", "BAR": "baz"}
280
+ result = evaluate_modifiers(expression, environ)
281
+ self.assertEqual(result, "/foo/baz")
282
+
283
+ def test_embedded_substitution_default_three_vars(self):
284
+ """Test embedded substitution with three vars."""
285
+ expression = "${VAR:=/${FOO}/${BAR}/${BAZ:=baz}}"
286
+ environ = {"FOO": "foo", "BAR": "bar"}
287
+ result = evaluate_modifiers(expression, environ)
288
+ self.assertEqual(result, "/foo/bar/baz")
289
+
290
+ def test_embedded_substitution_default_three_vars_alt_1(self):
291
+ """Test embedded substitution with three vars, atl 1."""
292
+ expression = "${VAR:=/${FOO}}/${BAR}/${BAZ:=baz}"
293
+ environ = {"FOO": "foo", "BAR": "bar"}
294
+ result = evaluate_modifiers(expression, environ)
295
+ self.assertEqual(result, "/foo/bar/baz")
296
+
297
+ def test_embedded_substitution_default_two_vars_from_env(self):
298
+ """Test embedded substitution with default, value from environ."""
299
+ expression = "${VAR:=${FOO}/${BAR}}"
300
+ environ = {"VAR": "/env/value", "FOO": "foo", "BAR": "bar"}
301
+ result = evaluate_modifiers(expression, environ)
302
+ self.assertEqual(result, "/env/value")
240
303
 
241
304
 
242
305
  class TestUtils(unittest.TestCase):
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